|
| 1 | +--- |
| 2 | +title: "HttpClientFactory uses SocketsHttpHandler as primary handler" |
| 3 | +description: Learn about the breaking change in networking in .NET 9 where HttpClientFactory now uses SocketsHttpHandler as the default primary handler. |
| 4 | +ms.date: 11/5/2024 |
| 5 | +--- |
| 6 | + |
| 7 | +# HttpClientFactory uses SocketsHttpHandler as primary handler |
| 8 | + |
| 9 | +`HttpClientFactory` allows you to configure an <xref:System.Net.Http.HttpMessageHandler> pipeline for named and typed <xref:System.Net.Http.HttpClient> objects. The inner-most handler, or the one that actually sends the request on the wire, is called a *primary handler*. If not configured, this handler was previously always an <xref:System.Net.Http.HttpClientHandler>. While the default primary handler is an implementation detail, there were users who depended on it. For example, some users cast the primary handler to `HttpClientHandler` to set properties like <xref:System.Net.Http.HttpClientHandler.ClientCertificates>, <xref:System.Net.Http.HttpClientHandler.UseCookies>, and <xref:System.Net.Http.HttpClientHandler.UseProxy>. |
| 10 | + |
| 11 | +With this change, the default primary handler is a <xref:System.Net.Http.SocketsHttpHandler> on platforms that support it. On other platforms, for example, .NET Framework, <xref:System.Net.Http.HttpClientHandler> continues to be used. |
| 12 | + |
| 13 | +`SocketsHttpHandler` now also has the <xref:System.Net.Http.SocketsHttpHandler.PooledConnectionLifetime> property preset to match the <xref:Microsoft.Extensions.Http.HttpClientFactoryOptions.HandlerLifetime> value. (It reflects the latest value, if `HandlerLifetime` was configured by the user). |
| 14 | + |
| 15 | +## Version introduced |
| 16 | + |
| 17 | +.NET 9 Preview 6 |
| 18 | + |
| 19 | +## Previous behavior |
| 20 | + |
| 21 | +The default primary handler was `HttpClientHandler`. Casting it to `HttpClientHandler` to update the properties happened to work. |
| 22 | + |
| 23 | +```csharp |
| 24 | +services.AddHttpClient("test") |
| 25 | + .ConfigurePrimaryHttpMessageHandler((h, _) => |
| 26 | + { |
| 27 | + ((HttpClientHandler)h).UseCookies = false; |
| 28 | + }); |
| 29 | + |
| 30 | +// This worked. |
| 31 | +var client = httpClientFactory.CreateClient("test"); |
| 32 | +``` |
| 33 | + |
| 34 | +## New behavior |
| 35 | + |
| 36 | +On platforms where `SocketsHttpHandler` is supported, the default primary handler is now `SocketsHttpHandler` with `PooledConnectionLifetime` set to the `HandlerLifetime` value. Casting it to `HttpClientHandler` to update the properties throws an <xref:System.InvalidCastException>. |
| 37 | + |
| 38 | +For example, the same code from the [Previous behavior](#previous-behavior) section now throws an <xref:System.InvalidCastException>: |
| 39 | + |
| 40 | +> System.InvalidCastException: Unable to cast object of type 'System.Net.Http.SocketsHttpHandler' to type 'System.Net.Http.HttpClientHandler'. |
| 41 | +
|
| 42 | +## Type of breaking change |
| 43 | + |
| 44 | +This change is a [behavioral change](../../categories.md#behavioral-change). |
| 45 | + |
| 46 | +## Reason for change |
| 47 | + |
| 48 | +One of the most common problems `HttpClientFactory` users run into is when a `Named` or `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 <xref:Microsoft.Extensions.Http.HttpClientFactoryOptions.HandlerLifetime>. Because `HttpClientFactory` can't rotate such handlers, they might end up not respecting DNS changes. |
| 49 | + |
| 50 | +This problem can be mitigated by using <xref:System.Net.Http.SocketsHttpHandler>, which has an option to control <xref:System.Net.Http.SocketsHttpHandler.PooledConnectionLifetime>. Similarly to <xref:Microsoft.Extensions.Http.HttpClientFactoryOptions.HandlerLifetime>, the pooled connection lifetime allows regularly recreating connections to pick up DNS changes, but on a lower level. A client with `PooledConnectionLifetime` set up can be safely used as a singleton. |
| 51 | + |
| 52 | +It is, unfortunately, easy and seemingly "intuitive" to inject a `Typed` client into a singleton. But it's hard to have any kind of check or analyzer to make sure `HttpClient` isn't captured when it wasn't supposed to be captured. It's also hard to troubleshoot the resulting issues. So as a preventative measure—to minimize the potential impact of erroneous usage patterns—the `SocketsHttpHandler` mitigation is now applied by default. |
| 53 | + |
| 54 | +This change only affects cases when the client wasn't configured by the end user to use a custom <xref:Microsoft.Extensions.Http.HttpMessageHandlerBuilder.PrimaryHandler> (for example, via <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.ConfigurePrimaryHttpMessageHandler``1(Microsoft.Extensions.DependencyInjection.IHttpClientBuilder)>). |
| 55 | + |
| 56 | +## Recommended action |
| 57 | + |
| 58 | +There are three options to work around the breaking change: |
| 59 | + |
| 60 | +- Explicitly specify and configure a primary handler for each of your clients: |
| 61 | + |
| 62 | + ```csharp |
| 63 | + services.AddHttpClient("test") |
| 64 | + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false }); |
| 65 | + ``` |
| 66 | + |
| 67 | +- Overwrite the default primary handler for all clients using <xref:Microsoft.Extensions.DependencyInjection.HttpClientFactoryServiceCollectionExtensions.ConfigureHttpClientDefaults(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{Microsoft.Extensions.DependencyInjection.IHttpClientBuilder})>: |
| 68 | + |
| 69 | + ```csharp |
| 70 | + services.ConfigureHttpClientDefaults(b => |
| 71 | + b.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false })); |
| 72 | + ``` |
| 73 | + |
| 74 | +- In the configuration action, check for both `HttpClientHandler` and `SocketsHttpHandler`: |
| 75 | + |
| 76 | + ```csharp |
| 77 | + services.AddHttpClient("test") |
| 78 | + .ConfigurePrimaryHttpMessageHandler((h, _) => |
| 79 | + { |
| 80 | + if (h is HttpClientHandler hch) |
| 81 | + { |
| 82 | + hch.UseCookies = false; |
| 83 | + } |
| 84 | + |
| 85 | + if (h is SocketsHttpHandler shh) |
| 86 | + { |
| 87 | + shh.UseCookies = false; |
| 88 | + } |
| 89 | + }); |
| 90 | + ``` |
| 91 | + |
| 92 | +## Affected APIs |
| 93 | + |
| 94 | +- <xref:Microsoft.Extensions.Http.HttpMessageHandlerBuilder.PrimaryHandler?displayProperty=fullName> |
| 95 | +- <xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.ConfigurePrimaryHttpMessageHandler(Microsoft.Extensions.DependencyInjection.IHttpClientBuilder,System.Action{System.Net.Http.HttpMessageHandler,System.IServiceProvider})?displayProperty=fullName> |
0 commit comments