Featured image of post Your custom HttpClient delegating handlers should be transient

Your custom HttpClient delegating handlers should be transient

Understand why reusing DelegatingHandlers in HttpClients with Microsoft.Extensions.Http could lead to catastrophic failures.

If you’ve ever encountered the following error, this article is for you:

InvalidOperationException: The ‘InnerHandler’ property must be null. ‘DelegatingHandler’ instances provided to ‘HttpMessageHandlerBuilder’ must not be reused or cached.

This means you are using the Microsoft.Extensions.Http library and have modified an HttpClient’s handler pipeline by inserting a DelegatingHandler. As the error indicates, you cannot reuse a handler across multiple HttpClient instances. In other words, your DelegatingHandler cannot be a singleton. Here’s an example of problematic code:

internal sealed class MyCustomDelegatingHandler : DelegatingHandler
{
    // Implementation omitted
}

// Registering the handler as a singleton is the mistake
services.AddSingleton<MyCustomDelegatingHandler>();

services.AddHttpClient("MyClient")
    .AddHttpMessageHandler<MyCustomDelegatingHandler>();

This code is also susceptible to the error mentioned above:

// This same instance will be used for all named HttpClient "MyClient"
var handler = new MyCustomDelegatingHandler();

services.AddHttpClient("MyClient")
    .AddHttpMessageHandler(() => handler);

Let’s try to understand what’s wrong here. The implementation of IHttpClientFactory, the service producing HttpClient instances, constructs the pipeline from multiple DelegatingHandlers making up an HttpClient. Each time an HttpClient is requested, the pipeline may be recreated.

HttpClient handler pipeline

Different DelegatingHandlers are linked to each other through the InnerHandler property, thanks to the CreateHandlerPipeline method. This method checks that the InnerHandler property isn’t already set and throws the InvalidOperationException mentioned above if it is.

Handlers created as part of the IHttpClientFactory implementation are temporarily stored in a cache, for a duration equal to HttpClientFactoryOptions.HandlerLifetime. By default, this duration is 2 minutes. During this time, as many HttpClients as needed can be created and used without issue.

However, once the handler is removed from the cache, it will go through the CreateHandlerPipeline method again, and if it is a singleton, the InnerHandler property will already be set, leading to the exception being thrown.

If you control the lifecycle of the HttpClient and only create one throughout the lifespan of your application, you might never notice this issue. But often, developers overlook this aspect and are not aware of the exact context in which their HttpClient is used.

A particularly noticeable and error-prone example is that of typed clients. Typed clients are registered using the generic AddHttpClient<TClient> method (and its overloads), but in my experience, few developers realize that the TClient type becomes registered in services with a transient lifecycle. Thus, requesting this TClient multiple times will result in the creation of multiple HttpClients, causing the pipeline construction error if one of your delegating handlers is a singleton.

The solution is simple: ensure that the handler factory passed to the AddHttpMessageHandler method always returns a new instance.

// Registering the handler as a transient is the fix
services.AddTransient<MyCustomDelegatingHandler>();

services.AddHttpClient("MyClient")
    .AddHttpMessageHandler<MyCustomDelegatingHandler>();

// Or explicitly create a new instance each time
services.AddHttpClient("MyClient")
    .AddHttpMessageHandler(() => new MyCustomDelegatingHandler());
Licensed under CC BY 4.0
Ko-fi donations Buy me a coffee