Description
HttpClientFactory allows you to configure HttpMessageHandler pipeline for named and typed HttpClients. The "inner-most" handler -- the one that actually sends the request on the wire -- is called a primary handler. If not configured, previously, this handler would always be an HttpClientHandler. While the default primary handler is an implementation detail, as it is never specified in the docs, there were users who depended on it, for example, casting the primary handler to HttpClientHandler to set properties like ClientCertificates, UseCookies, UseProxy etc.
The change makes the default primary handler to be a SocketsHttpHandler (on platforms that support it). Other platforms (e.g. .NET Framework) continue to use HttpClientHandler.
SocketsHttpHandler will also have the PooledConnectionLifetime property pre-set to match the HandlerLifetime value (it will reflect the latest value, if HandlerLifetime was configured by the user).
Introduced in .NET 9 Preview 6 (dotnet/runtime#101808)
Version
Other (please put exact version in description textbox)
Previous behavior
Default primary handler was HttpClientHandler. Casting it to HttpClientHandler to update the properties happened to work.
For example:
services.AddHttpClient("test")
.ConfigurePrimaryHttpMessageHandler((h, _) =>
{
((HttpClientHandler)h).UseCookies = false;
});
// ----
var client = httpClientFactory.CreateClient("test"); // works
New behavior
On platforms where SocketsHttpHandler is supported, default primary handler will be SocketsHttpHandler with PooledConnectionLifetime set to HandlerLifetime value. Casting it to HttpClientHandler to update the properties will throw.
For example, the same code as above
services.AddHttpClient("test")
.ConfigurePrimaryHttpMessageHandler((h, _) =>
{
((HttpClientHandler)h).UseCookies = false;
});
// ----
var client = httpClientFactory.CreateClient("test"); // throws
throws InvalidCastException
System.InvalidCastException: Unable to cast object of type 'System.Net.Http.SocketsHttpHandler'
to type 'System.Net.Http.HttpClientHandler'.
Type of breaking change
Reason for change
One of the most common problems HttpClientFactory users run into is when a Named or a Typed client erroneously gets captured in a Singleton service, or, in general, stored somewhere for a period of time that's longer than the specified HandlerLifetime. Because HttpClientFactory can't rotate such handlers, they might end up not respecting DNS changes.
This can be mitigated by using SocketsHttpHandler, which has an option to control PooledConnectionLifetime. Similarly to HandlerLifetime, it allows regularly recreating connections to pick up the DNS changes, but on lower level. A client with PooledConnectionLifetime set up can be safely used as a Singleton.
It is, unfortunately, very easy and seemingly "intuitive" to inject a Typed client into a singleton, but very hard to have any kind of check/analyzer to make sure HttpClient is not captured when it was not supposed to. It might be even harder to troubleshoot the resulting issues. So as a preventative measure -- to minimize the potential impact of such erroneous usage pattern -- the SocketsHttpHandler mitigation mentioned above is now applied by default.
This will only affect cases when the client was not configured by the end user to use a custom PrimaryHandler (via e.g. ConfigurePrimaryHttpMessageHandler<T>())
Recommended action
There are three options to workaround the breaking change.
- Explicitly specify and configure a Primary handler for each of your clients:
services.AddHttpClient("test")
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false });
- Overwrite default Primary handler for all clients using
ConfigureHttpClientDefaults:
services.ConfigureHttpClientDefaults(b =>
b.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false }));
- In the configuration action, check for both
HttpClientHandler and SocketsHttpHandler:
services.AddHttpClient("test")
.ConfigurePrimaryHttpMessageHandler((h, _) =>
{
if (h is HttpClientHandler hch)
{
hch.UseCookies = false;
}
if (h is SocketsHttpHandler shh)
{
shh.UseCookies = false;
}
});
Feature area
Extensions, Networking
Affected APIs
Microsoft.Extensions.Http.HttpMessageHandlerBuilder.PrimaryHandler property
Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.ConfigurePrimaryHttpMessageHandler(IHttpClientBuilder, Action<HttpMessageHandler,IServiceProvider>) specific overload that allows you to configure existing primary handler (instead of supplying a new one, like in other overloads)
Associated WorkItem - 340214
Description
HttpClientFactoryallows you to configureHttpMessageHandlerpipeline for named and typedHttpClients. The "inner-most" handler -- the one that actually sends the request on the wire -- is called a primary handler. If not configured, previously, this handler would always be anHttpClientHandler. While the default primary handler is an implementation detail, as it is never specified in the docs, there were users who depended on it, for example, casting the primary handler toHttpClientHandlerto set properties likeClientCertificates,UseCookies,UseProxyetc.The change makes the default primary handler to be a
SocketsHttpHandler(on platforms that support it). Other platforms (e.g. .NET Framework) continue to useHttpClientHandler.SocketsHttpHandlerwill also have thePooledConnectionLifetimeproperty pre-set to match theHandlerLifetimevalue (it will reflect the latest value, ifHandlerLifetimewas configured by the user).Introduced in .NET 9 Preview 6 (dotnet/runtime#101808)
Version
Other (please put exact version in description textbox)
Previous behavior
Default primary handler was
HttpClientHandler. Casting it toHttpClientHandlerto update the properties happened to work.For example:
New behavior
On platforms where
SocketsHttpHandleris supported, default primary handler will beSocketsHttpHandlerwithPooledConnectionLifetimeset toHandlerLifetimevalue. Casting it toHttpClientHandlerto update the properties will throw.For example, the same code as above
throws InvalidCastException
Type of breaking change
Reason for change
One of the most common problems
HttpClientFactoryusers run into is when a Named or a Typed client erroneously gets captured in a Singleton service, or, in general, stored somewhere for a period of time that's longer than the specifiedHandlerLifetime. BecauseHttpClientFactorycan't rotate such handlers, they might end up not respecting DNS changes.This can be mitigated by using
SocketsHttpHandler, which has an option to controlPooledConnectionLifetime. Similarly toHandlerLifetime, it allows regularly recreating connections to pick up the DNS changes, but on lower level. A client withPooledConnectionLifetimeset up can be safely used as a Singleton.It is, unfortunately, very easy and seemingly "intuitive" to inject a Typed client into a singleton, but very hard to have any kind of check/analyzer to make sure
HttpClientis not captured when it was not supposed to. It might be even harder to troubleshoot the resulting issues. So as a preventative measure -- to minimize the potential impact of such erroneous usage pattern -- theSocketsHttpHandlermitigation mentioned above is now applied by default.This will only affect cases when the client was not configured by the end user to use a custom PrimaryHandler (via e.g.
ConfigurePrimaryHttpMessageHandler<T>())Recommended action
There are three options to workaround the breaking change.
ConfigureHttpClientDefaults:HttpClientHandlerandSocketsHttpHandler:Feature area
Extensions, Networking
Affected APIs
Microsoft.Extensions.Http.HttpMessageHandlerBuilder.PrimaryHandlerpropertyMicrosoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.ConfigurePrimaryHttpMessageHandler(IHttpClientBuilder, Action<HttpMessageHandler,IServiceProvider>)specific overload that allows you to configure existing primary handler (instead of supplying a new one, like in other overloads)Associated WorkItem - 340214