[Bug]: Long-lived HttpClient instance despite use of IHttpClientFactory
Timovzl opened this issue ยท comments
Describe the bug ๐
Unless I have missed something, it appears that the current implementation does not let Refit be used correctly from singleton services.
I'm building on the assumption here that Refit does not intend to dictate whether its users use singleton, scoped, or transient services. I happen to be using singleton services and wish to start using Refit.
Refit does support the use of IHttpClientFactory
, which is great, but... it resolves a client from it once and keeps using that client.
IHttpClientFactory
's purpose is twofold:
- Avoid port exhaustion (which would occur by spawning one
HttpClient
per request), by reusingHttpMessageHandler
instances. - Observe DNS changes in a timely manner, by using new
HttpMessageHandler
instances periodically.
The only way in which IHttpClientFactory
can fulfill its purpose is if for every request (more or less) a new client is obtained from it. That is precisely what lets is balance the lifetime of each HttpMessageHandler
.
Since Refit obtains an HttpClient
from the factory once, the first purpose is achieved, but the second is circumvented, due to the HttpMessageHandler
being reused. I believe this goes against the intended usage pattern of IHttpClientFactory
.
Step to reproduce
We can see how the behavior to produce fresh instances is attempted here:
Refit uses a transient service. Whenever a new instance is requested, yes, a fresh HttpClient
is obtained. This only helps if the outer service making using of it is scoped or transient, but not if it has singleton lifetime.
Reproduction repository
https://github.com/reactiveui/refit
Expected behavior
The expected behavior is that Refit resolves a fresh HttpClient
from the IHttpClientFactory
whenever it is about to make a new request (instead of whenever it is resolved from the DI container).
Screenshots ๐ผ๏ธ
No response
IDE
No response
Operating system
No response
Version
No response
Device
No response
Refit Version
7.0.0
Additional information โน๏ธ
No response
From the recommended practices, we could choose the option of using a single, long-lived HttpClient
around a SocketsHttpHandler
with a relatively short PooledConnectionLifetime
, such as 2 minutes.
By creating the source-generated client around such an HttpClient
instance and registering the result as a singleton, we get correct and identical behavior across all possible parent service lifetimes.
var httpMessageHandler = new SocketsHttpHandler()
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
};
var httpClient = new HttpClient(httpMessageHandler)
{
BaseAddress = new Uri(uri.TrimEnd('/')),
};
var refitClient = RestService.For<IWhatever>(httpClient);
services.AddSingleton(refitClient);
If we're nitpicking and we'd also want the singleton client disposed when the container is disposed, we could even add the HttpClient
to the container under some obscure, unused name. The container will then adopt it.