88using System . Collections . Generic ;
99using System . Net ;
1010using System . Net . Http ;
11- using System . Net . Security ;
1211using System . Runtime . CompilerServices ;
1312using System . Threading ;
1413using 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