Common IHttpClientFactory
usage issues
In this article, you'll learn some of the most common problems you can run into when using IHttpClientFactory
to create HttpClient
instances.
IHttpClientFactory
is a convenient way to set up multiple HttpClient
configurations in the DI container, configure logging, set up resilience strategies, and more. IHttpClientFactory
also encapsulates the lifetime management of HttpClient
and HttpMessageHandler
instances, to prevent problems like socket exhaustion and losing DNS changes. For an overview on how to use IHttpClientFactory
in your .NET application, see IHttpClientFactory with .NET.
Due to a complex nature of IHttpClientFactory
integration with DI, you can hit some issues that might be hard to catch and troubleshoot. The scenarios listed in this article also contain recommendations, which you can apply proactively to avoid potential problems.
HttpClient
doesn't respect Scoped
lifetime
You can hit a problem if you need to access any scoped service, for example, HttpContext
, or some scoped cache, from within the HttpMessageHandler
. The data saved there can either "disappear", or, the other way around, "persist" when it shouldn't. This is caused by the Dependency Injection (DI) scope mismatch between the application context and the handler instance, and it's a known limitation in IHttpClientFactory
.
IHttpClientFactory
creates a separate DI scope per each HttpMessageHandler
instance. These handler scopes are distinct from application context scopes (for example, ASP.NET Core incoming request scope, or a user-created manual DI scope), so they will not share scoped service instances.
As a result of this limitation:
- Any data cached "externally" in a scoped service will not be available within the
HttpMessageHandler
. - Any data cached "internally" within the
HttpMessageHandler
or its scoped dependencies can be observed from multiple application DI scopes (for example, from different incoming requests) as they can share the same handler.
Consider the following recommendations to help alleviate this known limitation:
❌ DO NOT cache any scope-related information (such as data from HttpContext
) inside HttpMessageHandler
instances or its dependencies to avoid leaking sensitive information.
❌ DO NOT use cookies, as the CookieContainer
will be shared together with the handler.
✔️ CONSIDER not storing the information, or only pass it within the HttpRequestMessage
instance.
To pass arbitrary information alongside the HttpRequestMessage
, you can use the HttpRequestMessage.Options property.
✔️ CONSIDER encapsulating all the scope-related (for example, authentication) logic in a separate DelegatingHandler
that's not created by the IHttpClientFactory
, and use it to wrap the IHttpClientFactory
-created handler.
To create just an HttpMessageHandler
without HttpClient
, call IHttpMessageHandlerFactory.CreateHandler for any registered named client. In that case, you will need to create an HttpClient
instance yourself using the combined handler. You can find a fully runnable example for this workaround on GitHub.
For more information, see the Message Handler Scopes in IHttpClientFactory section in the IHttpClientFactory
guidelines.
HttpClient
doesn't respect DNS changes
Even if IHttpClientFactory
is used, it's still possible to hit the stale DNS problem. This can usually happen if an HttpClient
instance gets captured in a Singleton
service, or, in general, stored somewhere for a period of time that's longer than the specified HandlerLifetime
. HttpClient
will also get captured if the respective typed client is captured by a singleton.
❌ DO NOT cache HttpClient
instances created by IHttpClientFactory
for prolonged periods of time.
❌ DO NOT inject typed client instances into Singleton
services.
✔️ CONSIDER requesting a client from IHttpClientFactory
in a timely manner or each time you need one. Factory-created clients are safe to dispose.
HttpClient
instances created by IHttpClientFactory
are intended to be short-lived.
Recycling and recreating
HttpMessageHandler
's when their lifetime expires is essential forIHttpClientFactory
to ensure the handlers react to DNS changes.HttpClient
is tied to a specific handler instance upon its creation, so newHttpClient
instances should be requested in a timely manner to ensure the client will get the updated handler.Disposing of such
HttpClient
instances created by the factory will not lead to socket exhaustion, as its disposal does not trigger disposal of theHttpMessageHandler
.IHttpClientFactory
tracks and disposes of resources used to createHttpClient
instances, specifically theHttpMessageHandler
instances, as soon their lifetime expires and there's noHttpClient
using them anymore.
Typed clients are intended to be short-lived as well, as an HttpClient
instance is injected into the constructor, so it will share the typed client lifetime.
For more information, see the HttpClient
lifetime management and Avoid typed clients in singleton services sections in the IHttpClientFactory
guidelines.
HttpClient
uses too many sockets
Even if IHttpClientFactory
is used, it's still possible to hit the socket exhaustion issue with a specific usage scenario. By default, HttpClient
doesn't limit the number of concurrent requests. If a large number of HTTP/1.1 requests are started concurrently at the same time, each of them will end up triggering a new HTTP connection attempt, because there is no free connection in the pool and no limit is set.
❌ DO NOT start a large number of HTTP/1.1 requests concurrently at the same time without specifying the limits.
✔️ CONSIDER setting HttpClientHandler.MaxConnectionsPerServer (or SocketsHttpHandler.MaxConnectionsPerServer, if you use it as a primary handler) to a reasonable value. Note that these limits only apply to the specific handler instance.
✔️ CONSIDER using HTTP/2, which allows multiplexing requests over a single TCP connection.
Typed client has the wrong HttpClient
injected
There can be various situations in which it is possible to get an unexpected HttpClient
injected into a typed client. Most of the time, the root cause will be in an erroneous configuration, as, by DI design, any subsequent registration of a service overrides the previous one.
Typed clients use named clients "under the hood": adding a typed client implicitly registers and links it to a named client. The client name, unless explicitly provided, will be set to the type name of TClient
. This would be the first one from the TClient,TImplementation
pair if AddHttpClient<TClient,TImplementation>
overloads are used.
Therefore, registering a typed client does two separate things:
- Registers a named client (in a simple default case, the name is
typeof(TClient).Name
). - Registers a
Transient
service using theTClient
orTClient,TImplementation
provided.
The following two statements are technically the same:
services.AddHttpClient<ExampleClient>(c => c.BaseAddress = new Uri("http://example.com"));
// -OR-
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")) // register named client
.AddTypedClient<ExampleClient>(); // link the named client to a typed client
In a simple case, it will also be similar to the following:
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")); // register named client
// register plain Transient service and link it to the named client
services.AddTransient<ExampleClient>(s =>
new ExampleClient(
s.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ExampleClient))));
Consider the following examples of how the link between typed and named clients can get broken.
Typed client is registered a second time
❌ DO NOT register the typed client separately — it is already registered automatically by the AddHttpClient<T>
call.
If a typed client is erroneously registered a second time as a plain Transient service, this will overwrite the registration added by the HttpClientFactory
, breaking the link to the named client. It will manifest as if the HttpClient
's configuration is lost, as an unconfigured HttpClient
will get injected into the typed client instead.
It might be confusing that, instead of throwing an exception, a "wrong" HttpClient
is used. This happens because the "default" unconfigured HttpClient
— the client with the Options.DefaultName name (string.Empty
) — is registered as a plain Transient service, to enable the most basic HttpClientFactory
usage scenario. That's why after the link gets broken and the typed client becomes just an ordinary service, this "default" HttpClient
will naturally get injected into the respective constructor parameter.
Different typed clients are registered on a common interface
In case two different typed clients are registered on a common interface, they both would reuse the same named client. This can seem like the first typed client getting the second named client "wrongly" injected.
❌ DO NOT register multiple typed clients on a single interface without explicitly specifying the name.
✔️ CONSIDER registering and configuring a named client separately, and then linking it to one or multiple typed clients, either by specifying the name in AddHttpClient<T>
call or by calling AddTypedClient
during the named client setup.
By design, registering and configuring a named client with the same name several times just appends the configuration actions to the list of existing ones. This behavior of HttpClientFactory
might not be obvious, but it is the same approach that is used by the Options pattern and configuration APIs like Configure.
This is mostly useful for advanced handler configurations, for example, adding a custom handler to a named client defined externally, or mocking a primary handler for tests, but it works for HttpClient
instance configuration as well. For example, the three following examples will result in an HttpClient
configured in the same way (both BaseAddress
and DefaultRequestHeaders
are set):
// one configuration callback
services.AddHttpClient("example", c =>
{
c.BaseAddress = new Uri("http://example.com");
c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0");
});
// -OR-
// two configuration callbacks
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"))
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
// -OR-
// two configuration callbacks in separate AddHttpClient calls
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient("example")
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
This enables linking a typed client to an already defined named client, and also linking several typed clients to a single named client. It is more obvious when overloads with a name
parameter are used:
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));
services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");
The same thing can also be achieved by calling AddTypedClient during the named client configuration:
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
.AddTypedClient<FooLogger>()
.AddTypedClient<BarLogger>();
However, if you don't want to reuse the same named client, you but you still wish to register the clients on the same interface, you can do so by explicitly specifying different names for them:
services.AddHttpClient<ITypedClient, ExampleClient>(nameof(ExampleClient),
c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient<ITypedClient, GithubClient>(nameof(GithubClient),
c => c.BaseAddress = new Uri("https://github.com"));