reactiveui / refit

The automatic type-safe REST library for .NET Core, Xamarin and .NET. Heavily inspired by Square's Retrofit library, Refit turns your REST API into a live interface.

Home Page:https://reactiveui.github.io/refit/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[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:

  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

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.