Skip to content

Commit ea553fe

Browse files
committed
Improve stability of handlerkey
1 parent 4c5798e commit ea553fe

File tree

2 files changed

+62
-19
lines changed

2 files changed

+62
-19
lines changed

src/HandlerKey.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System.Net;
22
using System.Net.Http;
3-
using System.Net.Security;
43

54
namespace Soenneker.Utils.HttpClientCache;
65

@@ -16,7 +15,5 @@ internal readonly record struct HandlerKey(
1615
long? KeepAlivePingTimeoutTicks,
1716
HttpKeepAlivePingPolicy? KeepAlivePingPolicy,
1817
bool? UseProxy,
19-
IWebProxy? Proxy,
2018
int? MaxResponseDrainSize,
21-
int? MaxResponseHeadersLength,
22-
SslClientAuthenticationOptions? SslOptions);
19+
int? MaxResponseHeadersLength);

src/HttpClientCache.cs

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using System.Collections.Generic;
99
using System.Net;
1010
using System.Net.Http;
11-
using System.Net.Security;
1211
using System.Runtime.CompilerServices;
1312
using System.Threading;
1413
using System.Threading.Tasks;
@@ -107,9 +106,16 @@ private HttpClient CreateHttpClient(HttpClientOptions? options)
107106
return options?.HttpClientHandler != null ? new HttpClient(options.HttpClientHandler, disposeHandler: false) : new HttpClient();
108107
}
109108

110-
return options?.HttpClientHandler != null
111-
? new HttpClient(options.HttpClientHandler, disposeHandler: false)
112-
: new HttpClient(GetOrCreateHandler(options), disposeHandler: false);
109+
if (options?.HttpClientHandler != null)
110+
return new HttpClient(options.HttpClientHandler, disposeHandler: false);
111+
112+
// If caller supplies per-client proxy/SSL options, do NOT put those into the shared handler cache key
113+
// (it’s a common source of unbounded handler growth when options instances are created per call).
114+
// Instead, create a dedicated handler and attach it to the HttpClient so it will be disposed when the client is disposed.
115+
if (options?.Proxy is not null || options?.SslOptions is not null)
116+
return new HttpClient(CreateHandler(options), disposeHandler: true);
117+
118+
return new HttpClient(GetOrCreateHandler(options), disposeHandler: false);
113119
}
114120

115121
// Remove async state machine when ModifyClient is null.
@@ -135,17 +141,13 @@ private SocketsHttpHandler GetOrCreateHandler(HttpClientOptions? options)
135141
// Do NOT tie connect timeout to request timeout.
136142
TimeSpan connectTimeout = options?.ConnectTimeout ?? _defaultConnectTimeout;
137143

138-
// Extract refs so they become part of the key (no closure, no hash-key risk)
139-
IWebProxy? proxy = options?.Proxy;
140-
SslClientAuthenticationOptions? sslOptions = options?.SslOptions;
141-
142144
var key = new HandlerKey(PooledConnectionLifetimeTicks: (options?.PooledConnectionLifetime ?? _defaultPooledLifetime).Ticks,
143145
MaxConnectionsPerServer: options?.MaxConnectionsPerServer ?? 40, UseCookies: options?.UseCookieContainer == true,
144146
ConnectTimeoutTicks: connectTimeout.Ticks, ResponseDrainTimeoutTicks: options?.ResponseDrainTimeout?.Ticks,
145147
AllowAutoRedirect: options?.AllowAutoRedirect, AutomaticDecompression: options?.AutomaticDecompression,
146148
KeepAlivePingDelayTicks: options?.KeepAlivePingDelay?.Ticks, KeepAlivePingTimeoutTicks: options?.KeepAlivePingTimeout?.Ticks,
147-
KeepAlivePingPolicy: options?.KeepAlivePingPolicy, UseProxy: options?.UseProxy, Proxy: proxy, MaxResponseDrainSize: options?.MaxResponseDrainSize,
148-
MaxResponseHeadersLength: options?.MaxResponseHeadersLength, SslOptions: sslOptions);
149+
KeepAlivePingPolicy: options?.KeepAlivePingPolicy, UseProxy: options?.UseProxy, MaxResponseDrainSize: options?.MaxResponseDrainSize,
150+
MaxResponseHeadersLength: options?.MaxResponseHeadersLength);
149151

150152
return _handlers.GetSync(key);
151153
}
@@ -184,17 +186,61 @@ private static SocketsHttpHandler CreateHandlerFromKey(in HandlerKey key)
184186
if (key.UseProxy.HasValue)
185187
handler.UseProxy = key.UseProxy.Value;
186188

187-
if (key.Proxy is not null)
188-
handler.Proxy = key.Proxy;
189-
190189
if (key.MaxResponseDrainSize.HasValue)
191190
handler.MaxResponseDrainSize = key.MaxResponseDrainSize.Value;
192191

193192
if (key.MaxResponseHeadersLength.HasValue)
194193
handler.MaxResponseHeadersLength = key.MaxResponseHeadersLength.Value;
195194

196-
if (key.SslOptions is not null)
197-
handler.SslOptions = key.SslOptions;
195+
return handler;
196+
}
197+
198+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
199+
private static SocketsHttpHandler CreateHandler(HttpClientOptions? options)
200+
{
201+
// Dedicated handler for per-client settings (proxy/SSL). Keep it consistent with CreateHandlerFromKey defaults.
202+
var handler = new SocketsHttpHandler
203+
{
204+
PooledConnectionLifetime = options?.PooledConnectionLifetime ?? _defaultPooledLifetime,
205+
MaxConnectionsPerServer = options?.MaxConnectionsPerServer ?? 40,
206+
ConnectTimeout = options?.ConnectTimeout ?? _defaultConnectTimeout
207+
};
208+
209+
if (options?.UseCookieContainer == true)
210+
handler.CookieContainer = new CookieContainer();
211+
212+
if (options?.ResponseDrainTimeout is { } responseDrainTimeout)
213+
handler.ResponseDrainTimeout = responseDrainTimeout;
214+
215+
if (options?.AllowAutoRedirect is { } allowAutoRedirect)
216+
handler.AllowAutoRedirect = allowAutoRedirect;
217+
218+
if (options?.AutomaticDecompression is { } decompression)
219+
handler.AutomaticDecompression = decompression;
220+
221+
if (options?.KeepAlivePingDelay is { } keepAlivePingDelay)
222+
handler.KeepAlivePingDelay = keepAlivePingDelay;
223+
224+
if (options?.KeepAlivePingTimeout is { } keepAlivePingTimeout)
225+
handler.KeepAlivePingTimeout = keepAlivePingTimeout;
226+
227+
if (options?.KeepAlivePingPolicy is { } keepAlivePingPolicy)
228+
handler.KeepAlivePingPolicy = keepAlivePingPolicy;
229+
230+
if (options?.UseProxy is { } useProxy)
231+
handler.UseProxy = useProxy;
232+
233+
if (options?.Proxy is not null)
234+
handler.Proxy = options.Proxy;
235+
236+
if (options?.MaxResponseDrainSize is { } maxResponseDrainSize)
237+
handler.MaxResponseDrainSize = maxResponseDrainSize;
238+
239+
if (options?.MaxResponseHeadersLength is { } maxResponseHeadersLength)
240+
handler.MaxResponseHeadersLength = maxResponseHeadersLength;
241+
242+
if (options?.SslOptions is not null)
243+
handler.SslOptions = options.SslOptions;
198244

199245
return handler;
200246
}

0 commit comments

Comments
 (0)