Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it fine to use one HttpClient instance for each host my application needs to talk to?

I know that, when using the Microsoft dependency injection container, the best practice to handle HttpClient instances is using the IHttpClientFactory interface provided by the Microsoft.Extensions.Http nuget package.

Unfortunately the classes implementing the IHttpClientFactory interface are not public (as you can verify here), so the only way to exploit this pattern is using the Microsoft dependency injection container (at least it's the only one that I know). Sometimes I need to maintain old applications using a different container, so I need to figure out a best practice even when the IHttpClientFactory approach cannot be used.

As explained in this famous article and confirmed in the Microsoft docs too the HttpClient class is designed to be instantiated once per application lifetime and reused across multiple HTTP calls. This can safely be done because the public methods used to issue HTTP calls are documented to be thread safe, so a singleton instance can be safely used. In this case, it is important to follow the tips given in this article in order to avoid issues related with DNS changes.

So far so good.

Sometimes it is handy to use properties like BaseAddress or DefaultRequestHeaders, which are not thread safe (at least, they are not documented to be thread safe, so I assume they are not) to configure the HttpClient instance.

This opens a question: what happens if I have a singleton HttpClient instance and somewhere in my code I use the property DefaultRequestHeaders to set some common HTTP request headers useful to call one of the host my application needs to communicate with ? This is potentially dangerous, because different hosts could require different values for the same request header (think of authentication as an example of that). Furthermore, modifying DefaultRequestHeaders concurrently from two threads could potentially mess up the internal state of the HttpClient instance, because of the lack of thread safety guarantees.

For all these reasons, I think that the best approach to use HttpClient (when IServiceCollection is not available) is the following:

  • create one instace of HttpClient for each host the application needs to communicate with. Every call to one specific host will then use the same instance of HttpClient. Concurrent calls to the same host are safe, because of the documented thread safety of methods used to perform calls.

  • create one service for each host the application needs to communicate with. The HttpClient instance is injected inside this service and the service itself is used as a singleton in the application. This service is used to abstract away the access to the host it is coupled with. Classes like this are fully testable as illustrated here.

  • the only point where instances of HttpClient are created and configured is the composition root of the application. The code in the composition root is single threaded, so it is safe to use properties like DefaultRequestHeaders to configure the HttpClient instances.

Do you see any problem in creating one instance of HttpClient per host to be called ?

I know that instantiating one HttpClient per request can lead to socket exhaustion and must be avoided, but I guess that having one instance per host is safe with regard to this problem (because the same instance is used for all the requests to the same host and I do not expect that a single application needs to talk with a large number of different hosts).

Do you agree ? Am I missing anything ?

like image 664
Enrico Massone Avatar asked Jan 25 '23 16:01

Enrico Massone


1 Answers

I know that, when using the Microsoft dependency injection container, the best practice to handle HttpClient instances is using the IHttpClientFactory interface provided by the Microsoft.Extensions.Http nuget package.

Correct.

Unfortunately the classes implementing the IHttpClientFactory interface are not public (as you can verify here), so the only way to exploit this pattern is using the Microsoft dependency injection container (at least it's the only one that I know). Sometimes I need to maintain old applications using a different container, so I need to figure out a best practice even when the IHttpClientFactory approach cannot be used.

Microsoft.Extensions.DependencyInjection ("MEDI") should be thought of a (simplistic) abstraction over multiple DI systems - it just so happens to come with its own basic DI container. You can use MEDI as a front for Unity, SimpleInject, Ninject, and others.

As explained in this famous article and confirmed in the Microsoft docs too the HttpClient class is designed to be instantiated once per application lifetime and reused across multiple HTTP calls.

Not exactly.

  • You don't want a singleton HttpClient used by all consumers of HttpClient in your application because different consumers might have different assumptions about (as you later point out) DefaultRequestHeaders and other HttpClient state. Some code may also assume that HttpClient is not using any DelegatingHandler instances either.
  • You also don't want any instances of HttpClient (created using its own parameterless constructor) with an unlimited lifetime because of how its default internal HttpClientHandler handles (or rather, doesn't handle) DNS changes. Hence why the default IHttpClientFactory imposes a lifetime limit of 2 minutes for each HttpClientHandler instance.

This opens a question: what happens if I have a singleton HttpClient instance and somewhere in my code I use the property DefaultRequestHeaders to set some common HTTP request headers useful to call one of the host my application needs to communicate with?

What happens? What happens is what you can expect: different consumers of the same HttpClient instance acting on wrong information - such as sending the wrong Authorization header to the wrong BaseAddress. This is why HttpClient instances should not be shared.

This is potentially dangerous, because different hosts could require different values for the same request header (think of authentication as an example of that). Furthermore, modifying DefaultRequestHeaders concurrently from two threads could potentially mess up the internal state of the HttpClient instance, because of the lack of thread safety guarantees.

This isn't necessarily a "Thread safety" issue - you can have a single-threaded application that abuses a singleton HttpClient this way and still have the same issue. The real issue is that different objects (the consumers of HttpClient) are assuming that they are the owner of the HttpClient when they aren't.

Unfortunately C# and .NET do not have a built-in way to declare and assert ownership or object lifetimes (hence why IDisposable is a bit of a mess today) - so we need to resort to different alternatives.

create one instace of HttpClient for each host the application needs to communicate with. Every call to one specific host will then use the same instance of HttpClient. Concurrent calls to the same host are safe, because of the documented thread safety of methods used to perform calls.

(By "host" I assume you mean HTTP "origin"). This is naive and won't work if you make different requests to the same service with different access-tokens (if the access-tokens are stored in DefaultRequestHeaders).

create one service for each host the application needs to communicate with. The HttpClient instance is injected inside this service and the service itself is used as a singleton in the application. This service is used to abstract away the access to the host it is coupled with. Classes like this are fully testable as illustrated here.

Again, don't think of HTTP services in terms of "hosts" - otherwise this has the same problem as above.

the only point where instances of HttpClient are created and configured is the composition root of the application. The code in the composition root is single threaded, so it is safe to use properties like DefaultRequestHeaders to configure the HttpClient instances.

I'm not sure how this helps either. Your consumers might be stateful.

Anyway, the real solution, imo, is to implement your own IHttpClientFactory (it can also be your own interface!). To simplify things, your consumers' constructors won't accept a HttpClient instance, but instead accept the IHttpClientFactory and call its CreateClient method in order to get their own privately-owned and stateful instance of HttpClient which then uses the pool of shared and stateless HttpClientHandler instances.

Using this approach:

  • Each consumer gets its own private instance of HttpClient that they can alter as they like - no worries about objects modifying instances that they don't own.
  • Each consumer's HttpClient instance does not need to be disposed - you can safely disregard the fact they implement IDisposable.

    • Without pooled handlers, each HttpClient instance owns its own handler, which must be disposed.
    • But with pooled handlers, as with this approach, the pool manages handler lifetime and clean-up, not the HttpClient instances.
    • Your code can call HttpClient.Dispose() if it really wants to (or you just want to make FxCop shut-up) but it wont do anything: the underlying HttpMessageHandler (PooledHttpClientHandler) has a NOOP dispose method.
  • Managing the lifetime of HttpClient is irrelevant because each HttpClient only owns its own mutable state like DefaultRequestHeaders and BaseAddress - so you can have transient, scoped, long-life'd or singleton HttpClient instances and it's okay because they all dip into the pool of HttpClientHandler instances only when they actually send a request.

Like so:

/// <summary>This service should be registered as a singleton, or otherwise have an unbounded lifetime.</summary>
public QuickAndDirtyHttpClientFactory : IHttpClientFactory // `IHttpClientFactory ` can be your own interface. You do NOT need to use `Microsoft.Extensions.Http`.
{
    private readonly HttpClientHandlerPool pool = new HttpClientHandlerPool();

    public HttpClient CreateClient( String name )
    {
        PooledHttpClientHandler pooledHandler = new PooledHttpClientHandler( name, this.pool );
        return new HttpClient( pooledHandler );
    }

    // Alternative, which allows consumers to set up their own DelegatingHandler chains without needing to configure them during DI setup.
    public HttpClient CreateClient( String name, Func<HttpMessageHandler, DelegatingHandler> createHandlerChain )
    {
        PooledHttpClientHandler pooledHandler = new PooledHttpClientHandler( name, this.pool );
        DelegatingHandler chain = createHandlerChain( pooledHandler );
        return new HttpClient( chain );
    }
}

internal class HttpClientHandlerPool
{
    public HttpClientHandler BorrowHandler( String name )
    {
        // Implementing this is an exercise for the reader.
        // Alternatively, I'm available as a consultant for a very high hourly rate :D
    }

    public void ReleaseHandler( String name, HttpClientHandler handler )
    {
        // Implementing this is an exercise for the reader.
    }
}

internal class PooledHttpClientHandler : HttpMessageHandler
{
    private readonly String name;
    private readonly HttpClientHandlerPool pool;

    public PooledHttpClientHandler( String name, HttpClientHandlerPool pool )
    {
        this.name = name;
        this.pool = pool ?? throw new ArgumentNullException(nameof(pool));
    }

    protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken )
    {
        HttpClientHandler handler = this.pool.BorrowHandler( this.name );
        try
        {
            return await handler.SendAsync( request, cancellationToken ).ConfigureAwait(false);
        }
        finally
        {
            this.pool.ReleaseHandler( this.name, handler );
        }
    }

    // Don't override `Dispose(Bool)` - don't need to.
}

Then each consuimer can use it like so:

public class Turboencabulator : IEncabulator
{
    private readonly HttpClient httpClient;

    public Turboencabulator( IHttpClientFactory hcf )
    {
        this.httpClient = hcf.CreateClient();
        this.httpClient.DefaultRequestHeaders.Add( "Authorization", "my-secret-bearer-token" );
        this.httpClient.BaseAddress = "https://api1.example.com";
    }

    public async InverseReactiveCurrent( UnilateralPhaseDetractor upd )
    {
        await this.httpClient.GetAsync( etc )
    }
}

public class SecretelyDivertDataToTheNsaEncabulator : IEncabulator
{
    private readonly HttpClient httpClientReal;
    private readonly HttpClient httpClientNsa;

    public SecretNsaClientService( IHttpClientFactory hcf )
    {
        this.httpClientReal = hcf.CreateClient();
        this.httpClientReal.DefaultRequestHeaders.Add( "Authorization", "a-different-secret-bearer-token" );
        this.httpClientReal.BaseAddress = "https://api1.example.com";

        this.httpClientNsa = hcf.CreateClient();
        this.httpClientNsa.DefaultRequestHeaders.Add( "Authorization", "TODO: it's on a postit note on my desk viewable from outside the building" );
        this.httpClientNsa.BaseAddress = "https://totallylegit.nsa.gov";
    }

    public async InverseReactiveCurrent( UnilateralPhaseDetractor upd )
    {
        await this.httpClientNsa.GetAsync( etc )
        await this.httpClientReal.GetAsync( etc )
    }
}
like image 185
Dai Avatar answered Jan 31 '23 10:01

Dai