Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Long-lived HttpClient instance despite use of IHttpClientFactory #1646

Open
Timovzl opened this issue Jan 30, 2024 · 1 comment
Open
Labels

Comments

@Timovzl
Copy link

Timovzl commented Jan 30, 2024

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:

  1. Avoid port exhaustion (which would occur by spawning one HttpClient per request), by reusing HttpMessageHandler instances.
  2. 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:

builder.Services.AddTransient(

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

@Timovzl
Copy link
Author

Timovzl commented Jan 30, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant