Skip to content

Commit 48f260e

Browse files
authored
Make SocketsHttpHandler the default primary handler from HttpClientFactory (#101808)
Today the default handler is HttpClientHandler, which is just a wrapper around SocketsHttpHandler on platforms where SocketsHttpHandler is supported. This means that the configuration possible on SocketsHttpHandler isn't available as part of the default handling, which means consumers get the default PooledConnectionLifetime of infinite. The lack of that was one of the main motivations behind HttpClientFactory's handler lifetime and handler recycling. Instead of constructing an HttpClientHandler as the default handler, we can construct a SocketsHttpHandler as the default handler, and we can set its PooledConnectionLifetime to match the HttpClientFactoryOptions.HandlerLifetime, whether its default of 2 minutes or whatever a user configured it to be. This in turn means that if code gets and holds on to a handler/client from the factory for a prolonged period of time, it's still getting the connection recycling according to its configured options.
1 parent 4c27290 commit 48f260e

File tree

3 files changed

+88
-9
lines changed

3 files changed

+88
-9
lines changed

src/libraries/Microsoft.Extensions.Http/src/DefaultHttpMessageHandlerBuilder.cs

+42-3
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@
55
using System.Collections.Generic;
66
using System.Diagnostics.CodeAnalysis;
77
using System.Net.Http;
8+
using System.Threading;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Options;
811

912
namespace Microsoft.Extensions.Http
1013
{
1114
internal sealed class DefaultHttpMessageHandlerBuilder : HttpMessageHandlerBuilder
1215
{
16+
private HttpMessageHandler? _primaryHandler;
17+
private string? _name;
18+
1319
public DefaultHttpMessageHandlerBuilder(IServiceProvider services)
1420
{
1521
Services = services;
1622
}
1723

18-
private string? _name;
19-
2024
[DisallowNull]
2125
public override string? Name
2226
{
@@ -28,7 +32,11 @@ public override string? Name
2832
}
2933
}
3034

31-
public override HttpMessageHandler PrimaryHandler { get; set; } = new HttpClientHandler();
35+
public override HttpMessageHandler PrimaryHandler
36+
{
37+
get => _primaryHandler ??= CreatePrimaryHandler();
38+
set => _primaryHandler = value;
39+
}
3240

3341
public override IList<DelegatingHandler> AdditionalHandlers { get; } = new List<DelegatingHandler>();
3442

@@ -44,5 +52,36 @@ public override HttpMessageHandler Build()
4452

4553
return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers);
4654
}
55+
56+
#pragma warning disable CA1822, CA1859 // Mark members as static, Use concrete types when possible for improved performance
57+
private HttpMessageHandler CreatePrimaryHandler()
58+
#pragma warning restore CA1822, CA1859
59+
{
60+
#if NET
61+
// On platforms where SocketsHttpHandler is supported, HttpClientHandler is a thin wrapper
62+
// around it. By using SocketsHttpHandler directly, we can avoid the overhead of the wrapper,
63+
// but more importantly, we can configure it to limit the lifetime of its pooled connections
64+
// to match the requested lifetime of the handler itself. That way, if/when someone holds on
65+
// to a resulting HttpClient for a prolonged period of time, it'll still benefit from connection
66+
// recycling, and without needing to tear down and reconstitute the rest of the handler pipeline.
67+
if (SocketsHttpHandler.IsSupported)
68+
{
69+
SocketsHttpHandler handler = new();
70+
71+
if (Services.GetService<IOptionsMonitor<HttpClientFactoryOptions>>() is IOptionsMonitor<HttpClientFactoryOptions> optionsMonitor)
72+
{
73+
TimeSpan lifetime = optionsMonitor.Get(_name).HandlerLifetime;
74+
if (lifetime >= TimeSpan.Zero)
75+
{
76+
handler.PooledConnectionLifetime = lifetime;
77+
}
78+
}
79+
80+
return handler;
81+
}
82+
#endif
83+
84+
return new HttpClientHandler();
85+
}
4786
}
4887
}

src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DefaultHttpMessageHandlerBuilderTest.cs

+13-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,16 @@ public void Ctor_SetsPrimaryHandler()
2828
var builder = new DefaultHttpMessageHandlerBuilder(Services);
2929

3030
// Act
31-
Assert.IsType<HttpClientHandler>(builder.PrimaryHandler);
31+
#if NET
32+
if (SocketsHttpHandler.IsSupported)
33+
{
34+
Assert.IsType<SocketsHttpHandler>(builder.PrimaryHandler);
35+
}
36+
else
37+
#endif
38+
{
39+
Assert.IsType<HttpClientHandler>(builder.PrimaryHandler);
40+
}
3241
}
3342

3443
// Moq heavily utilizes RefEmit, which does not work on most aot workloads
@@ -78,7 +87,7 @@ public void Build_SomeAdditionalHandlers_PutsTogetherDelegatingHandlers()
7887

7988
[Fact]
8089
[ActiveIssue("https://github.com/dotnet/runtime/issues/50873", TestPlatforms.Android)]
81-
public void Build_PrimaryHandlerIsNull_ThrowsException()
90+
public void Build_PrimaryHandlerIsNull_UsesDefault()
8291
{
8392
// Arrange
8493
var builder = new DefaultHttpMessageHandlerBuilder(Services)
@@ -87,8 +96,8 @@ public void Build_PrimaryHandlerIsNull_ThrowsException()
8796
};
8897

8998
// Act & Assert
90-
var exception = Assert.Throws<InvalidOperationException>(() => builder.Build());
91-
Assert.Equal("The 'PrimaryHandler' must not be null.", exception.Message);
99+
var handler = builder.Build();
100+
Assert.NotNull(handler);
92101
}
93102

94103
[Fact]

src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs

+33-2
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,41 @@ public void UseSocketsHttpHandler_Parameterless_Success()
4242
var defaultPrimaryHandlerChain = messageHandlerFactory.CreateHandler("DefaultPrimaryHandler");
4343
var socketsHttpHandlerChain = messageHandlerFactory.CreateHandler("SocketsHttpHandler");
4444

45-
Assert.IsType<HttpClientHandler>(GetPrimaryHandler(defaultPrimaryHandlerChain));
45+
Assert.IsType<SocketsHttpHandler>(GetPrimaryHandler(defaultPrimaryHandlerChain));
4646
Assert.IsType<SocketsHttpHandler>(GetPrimaryHandler(socketsHttpHandlerChain));
4747
}
4848

49+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
50+
public void DefaultPrimaryHandler_RespectsHandlerLifetime()
51+
{
52+
var serviceCollection = new ServiceCollection();
53+
serviceCollection.AddHttpClient();
54+
serviceCollection.Configure<HttpClientFactoryOptions>(options => options.HandlerLifetime = TimeSpan.FromMinutes(42));
55+
56+
var services = serviceCollection.BuildServiceProvider();
57+
var messageHandlerFactory = services.GetRequiredService<IHttpMessageHandlerFactory>();
58+
var defaultPrimaryHandlerChain = messageHandlerFactory.CreateHandler();
59+
60+
SocketsHttpHandler handler = Assert.IsType<SocketsHttpHandler>(GetPrimaryHandler(defaultPrimaryHandlerChain));
61+
Assert.Equal(TimeSpan.FromMinutes(42), handler.PooledConnectionLifetime);
62+
}
63+
64+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
65+
public void DefaultPrimaryHandler_NamedClient_RespectsHandlerLifetime()
66+
{
67+
var serviceCollection = new ServiceCollection();
68+
serviceCollection.AddHttpClient("Configured");
69+
serviceCollection.Configure<HttpClientFactoryOptions>("Configured1", options => options.HandlerLifetime = TimeSpan.FromMinutes(42));
70+
serviceCollection.Configure<HttpClientFactoryOptions>("Configured2", options => options.HandlerLifetime = TimeSpan.FromMinutes(84));
71+
72+
var services = serviceCollection.BuildServiceProvider();
73+
var messageHandlerFactory = services.GetRequiredService<IHttpMessageHandlerFactory>();
74+
75+
Assert.Equal(TimeSpan.FromMinutes(42), Assert.IsType<SocketsHttpHandler>(GetPrimaryHandler(messageHandlerFactory.CreateHandler("Configured1"))).PooledConnectionLifetime);
76+
Assert.Equal(TimeSpan.FromMinutes(84), Assert.IsType<SocketsHttpHandler>(GetPrimaryHandler(messageHandlerFactory.CreateHandler("Configured2"))).PooledConnectionLifetime);
77+
Assert.Equal(TimeSpan.FromMinutes(2), Assert.IsType<SocketsHttpHandler>(GetPrimaryHandler(messageHandlerFactory.CreateHandler())).PooledConnectionLifetime);
78+
}
79+
4980
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
5081
public void UseSocketsHttpHandler_ConfiguredByAction_Success()
5182
{
@@ -66,7 +97,7 @@ public void UseSocketsHttpHandler_ConfiguredByAction_Success()
6697
var unconfiguredHandler = (SocketsHttpHandler)GetPrimaryHandler(unconfiguredHandlerChain);
6798
var configuredHandler = (SocketsHttpHandler)GetPrimaryHandler(configuredHandlerChain);
6899

69-
Assert.Equal(Timeout.InfiniteTimeSpan, unconfiguredHandler.PooledConnectionLifetime);
100+
Assert.Equal(TimeSpan.FromMinutes(2), unconfiguredHandler.PooledConnectionLifetime);
70101
Assert.Equal(TimeSpan.FromMinutes(1), configuredHandler.PooledConnectionLifetime);
71102
}
72103

0 commit comments

Comments
 (0)