< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 1761
Coverable lines: 1761
Total lines: 3029
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 824
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
File 1: .cctor()100%110%
File 1: .ctor(...)0%49490%
File 1: ConstructSslOptions(...)0%12120%
File 1: SendAsync(...)0%12120%
File 1: SendWithProxyAuthAsync(...)0%440%
File 1: SendWithNtConnectionAuthAsync(...)0%440%
File 1: SendWithNtProxyAuthAsync(...)0%440%
File 1: SendWithVersionDetectionAndRetryAsync()0%74740%
File 1: ConnectAsync()0%29290%
File 1: GetRemoteEndPoint(System.IO.Stream)0%440%
File 1: ConnectToTcpHostAsync()0%14140%
File 1: GetSslOptionsForRequest(...)0%10100%
File 1: ApplyPlaintextFilterAsync()0%880%
File 1: EstablishProxyTunnelAsync()0%440%
File 1: EstablishSocksTunnel()100%110%
File 1: GetConnectTimeoutCancellationTokenSource(...)0%220%
File 1: CreateConnectTimeoutException(...)100%110%
File 1: ThrowGetVersionException(...)0%660%
File 1: CheckExpirationOnGet(...)0%220%
File 1: CheckExpirationOnReturn(...)0%440%
File 1: Dispose()0%22220%
File 1: CleanCacheAndDisposeIfUnused()0%14140%
File 1: ToString()0%10100%
File 1: Trace(...)100%110%
File 2: .ctor(...)100%110%
File 2: TryGetPooledHttp11Connection(...)0%12120%
File 2: ProcessHttp11RequestQueue(...)0%30300%
File 2: CheckForHttp11ConnectionInjection()0%880%
File 2: InjectNewHttp11ConnectionAsync()0%10100%
File 2: CreateHttp11ConnectionAsync()100%110%
File 2: ConstructHttp11ConnectionAsync()100%110%
File 2: HandleHttp11ConnectionFailure(...)0%440%
File 2: RecycleHttp11Connection(...)0%440%
File 2: AddNewHttp11Connection(...)0%880%
File 2: ReturnHttp11Connection(...)0%440%
File 2: InvalidateHttp11Connection(...)0%220%
File 2: ScavengeHttp11ConnectionStack(...)0%880%
File 3: TryGetPooledHttp2Connection(...)0%30300%
File 3: CheckForHttp2ConnectionInjection()0%16160%
File 3: InjectNewHttp2ConnectionAsync()0%18180%
File 3: ConstructHttp2ConnectionAsync()0%440%
File 3: HandleHttp2ConnectionFailure(...)0%220%
File 3: HandleHttp11Downgrade()0%12120%
File 3: ReturnHttp2Connection(...)0%54540%
File 3: DisableHttp2Connection(...)0%220%
File 3: DisableHttp2ConnectionAsync()0%880%
File 3: InvalidateHttp2Connection(...)0%880%
File 3: HeartBeat()0%660%
File 3: ScavengeHttp2ConnectionList(...)0%16160%
File 4: .ctor(...)100%110%
File 4: TrySendUsingHttp3Async()0%14140%
File 4: TryGetPooledHttp3Connection(...)0%22220%
File 4: CheckForHttp3ConnectionInjection()0%16160%
File 4: InjectNewHttp3ConnectionAsync()0%34340%
File 4: HandleHttp3ConnectionFailure(...)0%440%
File 4: ReturnHttp3Connection(...)0%52520%
File 4: DisableHttp3Connection(...)0%220%
File 4: DisableHttp3ConnectionAsync()0%880%
File 4: InvalidateHttp3Connection(...)0%880%
File 4: ScavengeHttp3ConnectionList(...)0%16160%
File 4: TryGetHttp3Authority(...)0%10100%
File 4: ProcessAltSvc(...)0%440%
File 4: HandleAltSvc(...)0%36360%
File 4: ExpireAltSvcAuthority()100%110%
File 4: IsAltSvcBlocked(...)0%220%
File 4: BlocklistAuthority(...)0%20200%
File 4: OnNetworkChanged()0%880%

File(s)

D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\ConnectionPool\HttpConnectionPool.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Collections.Generic;
 5using System.Diagnostics;
 6using System.Diagnostics.CodeAnalysis;
 7using System.IO;
 8using System.Net.Http.Headers;
 9using System.Net.Http.HPack;
 10using System.Net.Http.QPack;
 11using System.Net.Quic;
 12using System.Net.Security;
 13using System.Net.Sockets;
 14using System.Runtime.CompilerServices;
 15using System.Runtime.ExceptionServices;
 16using System.Security.Authentication;
 17using System.Text;
 18using System.Threading;
 19using System.Threading.Tasks;
 20
 21namespace System.Net.Http
 22{
 23    /// <summary>Provides a pool of connections to the same endpoint.</summary>
 24    internal sealed partial class HttpConnectionPool : IDisposable
 25    {
 26        /// <summary>The maximum number of times to retry a request after a failure on an established connection.</summa
 27        private const int MaxConnectionFailureRetries = 3;
 28        public const int DefaultHttpPort = 80;
 29        public const int DefaultHttpsPort = 443;
 30
 031        private static readonly List<SslApplicationProtocol> s_http3ApplicationProtocols = new List<SslApplicationProtoc
 032        private static readonly List<SslApplicationProtocol> s_http2ApplicationProtocols = new List<SslApplicationProtoc
 033        private static readonly List<SslApplicationProtocol> s_http2OnlyApplicationProtocols = new List<SslApplicationPr
 34
 35        private readonly HttpConnectionPoolManager _poolManager;
 36        private readonly HttpConnectionKind _kind;
 37        private readonly Uri? _proxyUri;
 38        private readonly string? _telemetryServerAddress;
 39
 40        /// <summary>The origin authority used to construct the <see cref="HttpConnectionPool"/>.</summary>
 41        private readonly HttpAuthority _originAuthority;
 42
 43        /// <summary>The User-Agent header to use when creating a CONNECT tunnel.</summary>
 44        private string? _connectTunnelUserAgent;
 45
 46        // These settings are advertised by the server via SETTINGS_MAX_HEADER_LIST_SIZE and SETTINGS_MAX_FIELD_SECTION_
 47        // If we had previous connections to the same host in this pool, memorize the last value seen.
 48        // This value is used as an initial value for new connections before they have a chance to observe the SETTINGS 
 49        // Doing so avoids immediately exceeding the server limit on the first request, potentially causing the connecti
 50        // 0 means there were no previous connections, or they hadn't advertised this limit.
 51        // There is no need to lock when updating these values - we're only interested in saving _a_ value, not necessar
 52        internal uint _lastSeenHttp2MaxHeaderListSize;
 53        internal uint _lastSeenHttp3MaxHeaderListSize;
 54
 55        /// <summary>Options specialized and cached for this pool and its key.</summary>
 56        private readonly SslClientAuthenticationOptions? _sslOptionsHttp11;
 57        private readonly SslClientAuthenticationOptions? _sslOptionsHttp2;
 58        private readonly SslClientAuthenticationOptions? _sslOptionsHttp2Only;
 59        private SslClientAuthenticationOptions? _sslOptionsHttp3;
 60        private readonly SslClientAuthenticationOptions? _sslOptionsProxy;
 61
 62        private readonly PreAuthCredentialCache? _preAuthCredentials;
 63
 64        /// <summary>Whether the pool has been used since the last time a cleanup occurred.</summary>
 065        private bool _usedSinceLastCleanup = true;
 66        /// <summary>Whether the pool has been disposed.</summary>
 67        private bool _disposed;
 68
 69        /// <summary>Initializes the pool.</summary>
 70        /// <param name="poolManager">The manager associated with this pool.</param>
 71        /// <param name="kind">The kind of HTTP connections stored in this pool.</param>
 72        /// <param name="host">The host with which this pool is associated.</param>
 73        /// <param name="port">The port with which this pool is associated.</param>
 74        /// <param name="sslHostName">The SSL host with which this pool is associated.</param>
 75        /// <param name="proxyUri">The proxy this pool targets (optional).</param>
 76        /// <param name="telemetryServerAddress">The value of the 'server.address' tag to be emitted by Metrics and Dist
 077        public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionKind kind, string? host, int port
 078        {
 079            _poolManager = poolManager;
 080            _kind = kind;
 081            _proxyUri = proxyUri;
 082            _maxHttp11Connections = Settings._maxConnectionsPerServer;
 083            _telemetryServerAddress = telemetryServerAddress;
 84
 85            // The only case where 'host' will not be set is if this is a Proxy connection pool.
 086            Debug.Assert(host is not null || (kind == HttpConnectionKind.Proxy && proxyUri is not null));
 087            _originAuthority = new HttpAuthority(host ?? proxyUri!.IdnHost, port);
 88
 089            _http2Enabled = _poolManager.Settings._maxHttpVersion >= HttpVersion.Version20;
 90
 091            if (GlobalHttpSettings.SocketsHttpHandler.AllowHttp3)
 092            {
 093                _http3Enabled = _poolManager.Settings._maxHttpVersion >= HttpVersion.Version30;
 094            }
 95
 096            switch (kind)
 97            {
 98                case HttpConnectionKind.Http:
 099                    Debug.Assert(host != null);
 0100                    Debug.Assert(port != 0);
 0101                    Debug.Assert(sslHostName == null);
 0102                    Debug.Assert(proxyUri == null);
 103
 0104                    _http3Enabled = false;
 0105                    break;
 106
 107                case HttpConnectionKind.Https:
 0108                    Debug.Assert(host != null);
 0109                    Debug.Assert(port != 0);
 0110                    Debug.Assert(sslHostName != null);
 0111                    Debug.Assert(proxyUri == null);
 0112                    break;
 113
 114                case HttpConnectionKind.Proxy:
 0115                    Debug.Assert(host == null);
 0116                    Debug.Assert(port == 0);
 0117                    Debug.Assert(sslHostName == null);
 0118                    Debug.Assert(proxyUri != null);
 119
 0120                    _http2Enabled = false;
 0121                    _http3Enabled = false;
 0122                    break;
 123
 124                case HttpConnectionKind.ProxyTunnel:
 0125                    Debug.Assert(host != null);
 0126                    Debug.Assert(port != 0);
 0127                    Debug.Assert(sslHostName == null);
 0128                    Debug.Assert(proxyUri != null);
 129
 0130                    _http2Enabled = false;
 0131                    _http3Enabled = false;
 0132                    break;
 133
 134                case HttpConnectionKind.SslProxyTunnel:
 0135                    Debug.Assert(host != null);
 0136                    Debug.Assert(port != 0);
 0137                    Debug.Assert(sslHostName != null);
 0138                    Debug.Assert(proxyUri != null);
 139
 0140                    _http3Enabled = false; // TODO: how do we tunnel HTTP3?
 0141                    break;
 142
 143                case HttpConnectionKind.ProxyConnect:
 0144                    Debug.Assert(host != null);
 0145                    Debug.Assert(port != 0);
 0146                    Debug.Assert(sslHostName == null);
 0147                    Debug.Assert(proxyUri != null);
 148
 149                    // Don't enforce the max connections limit on proxy tunnels; this would mean that connections to dif
 150                    // would compete for the same limited number of connections.
 151                    // We will still enforce this limit on the user of the tunnel (i.e. ProxyTunnel or SslProxyTunnel).
 0152                    _maxHttp11Connections = int.MaxValue;
 153
 0154                    _http2Enabled = false;
 0155                    _http3Enabled = false;
 0156                    break;
 157
 158                case HttpConnectionKind.SocksTunnel:
 159                case HttpConnectionKind.SslSocksTunnel:
 0160                    Debug.Assert(host != null);
 0161                    Debug.Assert(port != 0);
 0162                    Debug.Assert(proxyUri != null);
 163
 0164                    _http3Enabled = false; // TODO: SOCKS supports UDP and may be used for HTTP3
 0165                    break;
 166
 167                default:
 0168                    Debug.Fail("Unknown HttpConnectionKind in HttpConnectionPool.ctor");
 169                    break;
 170            }
 171
 0172            if (!_http3Enabled)
 0173            {
 174                // Avoid parsing Alt-Svc headers if they won't be used.
 0175                _altSvcEnabled = false;
 0176            }
 177
 0178            string? hostHeader = null;
 0179            if (host is not null)
 0180            {
 181                // Precalculate ASCII bytes for Host header
 182                // Note that if _host is null, this is a (non-tunneled) proxy connection, and we can't cache the hostnam
 0183                hostHeader = IsDefaultPort
 0184                    ? _originAuthority.HostValue
 0185                    : $"{_originAuthority.HostValue}:{_originAuthority.Port}";
 186
 187                // Note the IDN hostname should always be ASCII, since it's already been IDNA encoded.
 0188                byte[] hostHeaderLine = new byte[6 + hostHeader.Length + 2]; // Host: foo\r\n
 0189                "Host: "u8.CopyTo(hostHeaderLine);
 0190                Encoding.ASCII.GetBytes(hostHeader, hostHeaderLine.AsSpan(6));
 0191                hostHeaderLine[^2] = (byte)'\r';
 0192                hostHeaderLine[^1] = (byte)'\n';
 0193                _hostHeaderLineBytes = hostHeaderLine;
 194
 0195                Debug.Assert(Encoding.ASCII.GetString(_hostHeaderLineBytes) == $"Host: {hostHeader}\r\n");
 0196            }
 197
 0198            if (sslHostName != null)
 0199            {
 0200                _sslOptionsHttp11 = ConstructSslOptions(poolManager, sslHostName);
 0201                _sslOptionsHttp11.ApplicationProtocols = null;
 202
 0203                if (_http2Enabled)
 0204                {
 0205                    _sslOptionsHttp2 = ConstructSslOptions(poolManager, sslHostName);
 0206                    _sslOptionsHttp2.ApplicationProtocols = s_http2ApplicationProtocols;
 0207                    _sslOptionsHttp2Only = ConstructSslOptions(poolManager, sslHostName);
 0208                    _sslOptionsHttp2Only.ApplicationProtocols = s_http2OnlyApplicationProtocols;
 209
 210                    // Note:
 211                    // The HTTP/2 specification states:
 212                    //   "A deployment of HTTP/2 over TLS 1.2 MUST disable renegotiation.
 213                    //    An endpoint MUST treat a TLS renegotiation as a connection error (Section 5.4.1)
 214                    //    of type PROTOCOL_ERROR."
 215                    // which suggests we should do:
 216                    //   _sslOptionsHttp2.AllowRenegotiation = false;
 217                    // However, if AllowRenegotiation is set to false, that will also prevent
 218                    // renegotation if the server denies the HTTP/2 request and causes a
 219                    // downgrade to HTTP/1.1, and the current APIs don't provide a mechanism
 220                    // by which AllowRenegotiation could be set back to true in that case.
 221                    // For now, if an HTTP/2 server erroneously issues a renegotiation, we'll
 222                    // allow it.
 0223                }
 0224            }
 225
 0226            if (hostHeader is not null)
 0227            {
 0228                if (_http2Enabled)
 0229                {
 0230                    _http2EncodedAuthorityHostHeader = HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingToAllocatedAr
 0231                }
 232
 0233                if (GlobalHttpSettings.SocketsHttpHandler.AllowHttp3 && _http3Enabled)
 0234                {
 0235                    _http3EncodedAuthorityHostHeader = QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReferenceToArr
 0236                }
 0237            }
 238
 239            // Set up for PreAuthenticate.  Access to this cache is guarded by a lock on the cache itself.
 0240            if (_poolManager.Settings._preAuthenticate)
 0241            {
 0242                _preAuthCredentials = new PreAuthCredentialCache();
 0243            }
 244
 0245            _http11RequestQueue = new RequestQueue<HttpConnection>();
 0246            if (_http2Enabled)
 0247            {
 0248                _http2RequestQueue = new RequestQueue<Http2Connection?>();
 0249            }
 0250            if (GlobalHttpSettings.SocketsHttpHandler.AllowHttp3 && _http3Enabled)
 0251            {
 0252                _http3RequestQueue = new RequestQueue<Http3Connection?>();
 0253            }
 254
 0255            if (_proxyUri != null && HttpUtilities.IsSupportedSecureScheme(_proxyUri.Scheme))
 0256            {
 0257                _sslOptionsProxy = ConstructSslOptions(poolManager, _proxyUri.IdnHost);
 0258                _sslOptionsProxy.ApplicationProtocols = null;
 0259            }
 260
 0261            if (NetEventSource.Log.IsEnabled()) Trace($"{this}");
 0262        }
 263
 264        private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnectionPoolManager poolManager, string 
 0265        {
 0266            Debug.Assert(sslHostName != null);
 267
 0268            SslClientAuthenticationOptions sslOptions = poolManager.Settings._sslOptions?.ShallowClone() ?? new SslClien
 269
 270            // This is only set if we are underlying handler for HttpClientHandler
 0271            if (poolManager.Settings._clientCertificateOptions == ClientCertificateOption.Manual && sslOptions.LocalCert
 0272                    (sslOptions.ClientCertificates == null || sslOptions.ClientCertificates.Count == 0))
 0273            {
 274                // If we have no client certificates do not set callback when internal selection is used.
 275                // It breaks TLS resume on Linux
 0276                sslOptions.LocalCertificateSelectionCallback = null;
 0277            }
 278
 279            // Set TargetHost for SNI
 0280            sslOptions.TargetHost = sslHostName;
 281
 0282            return sslOptions;
 0283        }
 284
 0285        public string? TelemetryServerAddress => _telemetryServerAddress;
 0286        public HttpAuthority OriginAuthority => _originAuthority;
 0287        public HttpConnectionSettings Settings => _poolManager.Settings;
 0288        public HttpConnectionKind Kind => _kind;
 0289        public bool IsSecure => _kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind
 0290        public Uri? ProxyUri => _proxyUri;
 0291        public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials;
 0292        public PreAuthCredentialCache? PreAuthCredentials => _preAuthCredentials;
 0293        public bool IsDefaultPort => OriginAuthority.Port == (IsSecure ? DefaultHttpsPort : DefaultHttpPort);
 0294        private bool DoProxyAuth => (_kind == HttpConnectionKind.Proxy || _kind == HttpConnectionKind.ProxyConnect);
 295
 296        /// <summary>Object used to synchronize access to state in the pool.</summary>
 297        private object SyncObj
 298        {
 299            get
 0300            {
 0301                Debug.Assert(!Monitor.IsEntered(_http11Connections));
 0302                return _http11Connections;
 0303            }
 304        }
 305
 0306        public bool HasSyncObjLock => Monitor.IsEntered(_http11Connections);
 307
 308        // Overview of connection management (mostly HTTP version independent):
 309        //
 310        // Each version of HTTP (1.1, 2, 3) has its own connection pool, and each of these work in a similar manner,
 311        // allowing for differences between the versions (most notably, HTTP/1.1 is not multiplexed.)
 312        //
 313        // When a request is submitted for a particular version (e.g. HTTP/1.1), we first look in the pool for available
 314        // An "available" connection is one that is (hopefully) usable for a new request.
 315        //      For HTTP/1.1, this is just an idle connection.
 316        //      For HTTP2/3, this is a connection that (hopefully) has available streams to use for new requests.
 317        // If we find an available connection, we will attempt to validate it and then use it.
 318        //      We check the lifetime of the connection and discard it if the lifetime is exceeded.
 319        //      We check that the connection has not shut down; if so we discard it.
 320        //      For HTTP2/3, we reserve a stream on the connection. If this fails, we cannot use the connection right no
 321        // If validation fails, we will attempt to find a different available connection.
 322        //
 323        // Once we have found a usable connection, we use it to process the request.
 324        //      For HTTP/1.1, a connection can handle only a single request at a time, thus it is immediately removed fr
 325        //      For HTTP2/3, a connection is only removed from the available list when it has no more available streams.
 326        //      In either case, the connection still counts against the total associated connection count for the pool.
 327        //
 328        // If we cannot find a usable available connection, then the request is added the to the request queue for the a
 329        //
 330        // Whenever a request is queued, or an existing connection shuts down, we will check to see if we should inject 
 331        // Injection policy depends on both user settings and some simple heuristics.
 332        // See comments on the relevant routines for details on connection injection policy.
 333        //
 334        // When a new connection is successfully created, or an existing unavailable connection becomes available again,
 335        // we will attempt to use this connection to handle any queued requests (subject to lifetime restrictions on exi
 336        // This may result in the connection becoming unavailable again, because it cannot handle any more requests at t
 337        // If not, we will return the connection to the pool as an available connection for use by new requests.
 338        //
 339        // When a connection shuts down, either gracefully (e.g. GOAWAY) or abortively (e.g. IOException),
 340        // we will remove it from the list of available connections, if it is present there.
 341        // If not, then it must be unavailable at the moment; we will detect this and ensure it is not added back to the
 342
 343        public ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, bool doRequestAuth, Canc
 0344        {
 345            // We need the User-Agent header when we send a CONNECT request to the proxy.
 346            // We must read the header early, before we return the ownership of the request back to the user.
 0347            if ((Kind is HttpConnectionKind.ProxyTunnel or HttpConnectionKind.SslProxyTunnel) &&
 0348                request.HasHeaders &&
 0349                request.Headers.NonValidated.TryGetValues(HttpKnownHeaderNames.UserAgent, out HeaderStringValues userAge
 0350            {
 0351                _connectTunnelUserAgent = userAgent.ToString();
 0352            }
 353
 0354            if (doRequestAuth && Settings._credentials != null)
 0355            {
 0356                return AuthenticationHelper.SendWithRequestAuthAsync(request, async, Settings._credentials, Settings._pr
 357            }
 358
 0359            return SendWithProxyAuthAsync(request, async, doRequestAuth, cancellationToken);
 0360        }
 361
 362        public ValueTask<HttpResponseMessage> SendWithProxyAuthAsync(HttpRequestMessage request, bool async, bool doRequ
 0363        {
 0364            if (DoProxyAuth && ProxyCredentials is not null)
 0365            {
 0366                return AuthenticationHelper.SendWithProxyAuthAsync(request, _proxyUri!, async, ProxyCredentials, doReque
 367            }
 368
 0369            return SendWithVersionDetectionAndRetryAsync(request, async, doRequestAuth, cancellationToken);
 0370        }
 371
 372        private Task<HttpResponseMessage> SendWithNtConnectionAuthAsync(HttpConnection connection, HttpRequestMessage re
 0373        {
 0374            if (doRequestAuth && Settings._credentials != null)
 0375            {
 0376                return AuthenticationHelper.SendWithNtConnectionAuthAsync(request, async, Settings._credentials, Setting
 377            }
 378
 0379            return SendWithNtProxyAuthAsync(connection, request, async, cancellationToken);
 0380        }
 381
 382        public Task<HttpResponseMessage> SendWithNtProxyAuthAsync(HttpConnection connection, HttpRequestMessage request,
 0383        {
 0384            if (DoProxyAuth && ProxyCredentials is not null)
 0385            {
 0386                return AuthenticationHelper.SendWithNtProxyAuthAsync(request, ProxyUri!, async, ProxyCredentials, HttpHa
 387            }
 388
 0389            return connection.SendAsync(request, async, cancellationToken);
 0390        }
 391
 392        public async ValueTask<HttpResponseMessage> SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, bo
 0393        {
 0394            _usedSinceLastCleanup = true;
 395
 396            // Loop on connection failures (or other problems like version downgrade) and retry if possible.
 0397            int retryCount = 0;
 0398            while (true)
 0399            {
 0400                HttpConnectionWaiter<HttpConnection>? http11ConnectionWaiter = null;
 0401                HttpConnectionWaiter<Http2Connection?>? http2ConnectionWaiter = null;
 402                try
 0403                {
 0404                    HttpResponseMessage? response = null;
 405
 406                    // Use HTTP/3 if possible.
 0407                    if (GlobalHttpSettings.SocketsHttpHandler.AllowHttp3 && // guard to enable trimming HTTP/3 support
 0408                        _http3Enabled &&
 0409                        (request.Version.Major >= 3 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHighe
 0410                        !request.IsExtendedConnectRequest)
 0411                    {
 0412                        Debug.Assert(async);
 0413                        if (QuicConnection.IsSupported)
 0414                        {
 0415                            if (_sslOptionsHttp3 == null)
 0416                            {
 417                                // deferred creation. We use atomic exchange to be sure all threads point to single obje
 0418                                SslClientAuthenticationOptions sslOptionsHttp3 = ConstructSslOptions(_poolManager, _sslO
 0419                                sslOptionsHttp3.ApplicationProtocols = s_http3ApplicationProtocols;
 0420                                Interlocked.CompareExchange(ref _sslOptionsHttp3, sslOptionsHttp3, null);
 0421                            }
 422
 0423                            response = await TrySendUsingHttp3Async(request, cancellationToken).ConfigureAwait(false);
 0424                        }
 425                        else
 0426                        {
 0427                            _altSvcEnabled = false;
 0428                            _http3Enabled = false;
 0429                        }
 0430                    }
 431
 0432                    if (response is null)
 0433                    {
 434                        // We could not use HTTP/3. Do not continue if downgrade is not allowed.
 0435                        if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLow
 0436                        {
 0437                            ThrowGetVersionException(request, 3);
 0438                        }
 439
 440                        // Use HTTP/2 if possible.
 0441                        if (_http2Enabled &&
 0442                            (request.Version.Major >= 2 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrH
 0443                            (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower || IsSecure)) // prefer HT
 0444                        {
 0445                            if (!TryGetPooledHttp2Connection(request, out Http2Connection? connection, out http2Connecti
 0446                                http2ConnectionWaiter != null)
 0447                            {
 0448                                connection = await http2ConnectionWaiter.WaitForConnectionAsync(request, this, async, ca
 0449                            }
 450
 0451                            Debug.Assert(connection is not null || !_http2Enabled);
 0452                            if (connection is not null)
 0453                            {
 0454                                if (request.IsExtendedConnectRequest)
 0455                                {
 0456                                    await connection.InitialSettingsReceived.WaitWithCancellationAsync(cancellationToken
 0457                                    if (!connection.IsConnectEnabled)
 0458                                    {
 0459                                        HttpRequestException exception = new(HttpRequestError.ExtendedConnectNotSupporte
 0460                                        exception.Data["SETTINGS_ENABLE_CONNECT_PROTOCOL"] = false;
 0461                                        throw exception;
 462                                    }
 0463                                }
 464
 0465                                response = await connection.SendAsync(request, async, cancellationToken).ConfigureAwait(
 0466                            }
 0467                        }
 468
 0469                        if (response is null)
 0470                        {
 471                            // We could not use HTTP/2. Do not continue if downgrade is not allowed.
 0472                            if (request.Version.Major >= 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionO
 0473                            {
 0474                                ThrowGetVersionException(request, 2);
 0475                            }
 476
 477                            // Use HTTP/1.x.
 0478                            if (!TryGetPooledHttp11Connection(request, async, out HttpConnection? connection, out http11
 0479                            {
 0480                                connection = await http11ConnectionWaiter.WaitForConnectionAsync(request, this, async, c
 0481                            }
 482
 0483                            connection.Acquire(); // In case we are doing Windows (i.e. connection-based) auth, we need 
 484                            try
 0485                            {
 0486                                response = await SendWithNtConnectionAuthAsync(connection, request, async, doRequestAuth
 0487                            }
 488                            finally
 0489                            {
 0490                                connection.Release();
 0491                            }
 0492                        }
 0493                    }
 494
 0495                    ProcessAltSvc(response);
 0496                    return response;
 497                }
 0498                catch (HttpRequestException e) when (e.AllowRetry == RequestRetryType.RetryOnConnectionFailure)
 0499                {
 0500                    Debug.Assert(retryCount >= 0 && retryCount <= MaxConnectionFailureRetries);
 501
 0502                    if (retryCount == MaxConnectionFailureRetries)
 0503                    {
 0504                        if (NetEventSource.Log.IsEnabled())
 0505                        {
 0506                            Trace($"MaxConnectionFailureRetries limit of {MaxConnectionFailureRetries} hit. Retryable re
 0507                        }
 508
 0509                        throw;
 510                    }
 511
 0512                    retryCount++;
 513
 0514                    if (NetEventSource.Log.IsEnabled())
 0515                    {
 0516                        Trace($"Retry attempt {retryCount} after connection failure. Connection exception: {e}");
 0517                    }
 518
 519                    // Eat exception and try again.
 0520                }
 0521                catch (HttpRequestException e) when (e.AllowRetry == RequestRetryType.RetryOnLowerHttpVersion)
 0522                {
 523                    // Throw if fallback is not allowed by the version policy.
 0524                    if (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
 0525                    {
 0526                        throw new HttpRequestException(HttpRequestError.VersionNegotiationError, SR.Format(SR.net_http_r
 527                    }
 528
 0529                    if (NetEventSource.Log.IsEnabled())
 0530                    {
 0531                        Trace($"Retrying request because server requested version fallback: {e}");
 0532                    }
 533
 534                    // Eat exception and try again on a lower protocol version.
 0535                    request.Version = HttpVersion.Version11;
 0536                }
 537                finally
 0538                {
 539                    // We never cancel both attempts at the same time. When downgrade happens, it's possible that both w
 540                    // but in that case http2ConnectionWaiter.ConnectionCancellationTokenSource shall be null.
 0541                    Debug.Assert(http11ConnectionWaiter is null || http2ConnectionWaiter?.ConnectionCancellationTokenSou
 0542                    http11ConnectionWaiter?.SetTimeoutToPendingConnectionAttempt(this, cancellationToken.IsCancellationR
 0543                    http2ConnectionWaiter?.SetTimeoutToPendingConnectionAttempt(this, cancellationToken.IsCancellationRe
 0544                }
 0545            }
 0546        }
 547
 548        private async ValueTask<(Stream, TransportContext?, Activity?, IPEndPoint?)> ConnectAsync(HttpRequestMessage req
 0549        {
 0550            Stream? stream = null;
 0551            IPEndPoint? remoteEndPoint = null;
 0552            Exception? exception = null;
 0553            TransportContext? transportContext = null;
 554
 0555            Activity? activity = ConnectionSetupDistributedTracing.StartConnectionSetupActivity(IsSecure, _telemetryServ
 556
 557            try
 0558            {
 0559                switch (_kind)
 560                {
 561                    case HttpConnectionKind.Http:
 562                    case HttpConnectionKind.Https:
 563                    case HttpConnectionKind.ProxyConnect:
 0564                        stream = await ConnectToTcpHostAsync(_originAuthority.IdnHost, _originAuthority.Port, request, a
 565                        // remoteEndPoint is returned for diagnostic purposes.
 0566                        remoteEndPoint = GetRemoteEndPoint(stream);
 0567                        if (_kind == HttpConnectionKind.ProxyConnect && _sslOptionsProxy != null)
 0568                        {
 0569                            stream = await ConnectHelper.EstablishSslConnectionAsync(_sslOptionsProxy, request, async, s
 0570                        }
 0571                        break;
 572
 573                    case HttpConnectionKind.Proxy:
 0574                        stream = await ConnectToTcpHostAsync(_proxyUri!.IdnHost, _proxyUri.Port, request, async, cancell
 575                        // remoteEndPoint is returned for diagnostic purposes.
 0576                        remoteEndPoint = GetRemoteEndPoint(stream);
 0577                        if (_sslOptionsProxy != null)
 0578                        {
 0579                            stream = await ConnectHelper.EstablishSslConnectionAsync(_sslOptionsProxy, request, async, s
 0580                        }
 0581                        break;
 582
 583                    case HttpConnectionKind.ProxyTunnel:
 584                    case HttpConnectionKind.SslProxyTunnel:
 0585                        stream = await EstablishProxyTunnelAsync(async, cancellationToken).ConfigureAwait(false);
 586
 0587                        if (stream is HttpContentStream contentStream && contentStream._connection?._stream is Stream in
 0588                        {
 0589                            remoteEndPoint = GetRemoteEndPoint(innerStream);
 0590                        }
 591
 0592                        break;
 593
 594                    case HttpConnectionKind.SocksTunnel:
 595                    case HttpConnectionKind.SslSocksTunnel:
 0596                        stream = await EstablishSocksTunnel(request, async, cancellationToken).ConfigureAwait(false);
 597                        // remoteEndPoint is returned for diagnostic purposes.
 0598                        remoteEndPoint = GetRemoteEndPoint(stream);
 0599                        break;
 600                }
 601
 0602                Debug.Assert(stream != null);
 603
 0604                if (IsSecure)
 0605                {
 0606                    SslStream? sslStream = stream as SslStream;
 0607                    if (sslStream == null)
 0608                    {
 0609                        sslStream = await ConnectHelper.EstablishSslConnectionAsync(GetSslOptionsForRequest(request), re
 0610                    }
 611                    else
 0612                    {
 0613                        if (NetEventSource.Log.IsEnabled())
 0614                        {
 0615                            Trace($"Connected with custom SslStream: alpn='${sslStream.NegotiatedApplicationProtocol}'")
 0616                        }
 0617                    }
 0618                    transportContext = sslStream.TransportContext;
 0619                    stream = sslStream;
 0620                }
 0621            }
 0622            catch (Exception ex) when (activity is not null)
 0623            {
 0624                exception = ex;
 0625                throw;
 626            }
 627            finally
 0628            {
 0629                if (activity is not null)
 0630                {
 0631                    ConnectionSetupDistributedTracing.StopConnectionSetupActivity(activity, exception, remoteEndPoint);
 0632                }
 0633            }
 634
 0635            return (stream, transportContext, activity, remoteEndPoint);
 636
 0637            static IPEndPoint? GetRemoteEndPoint(Stream stream) => (stream as NetworkStream)?.Socket?.RemoteEndPoint as 
 0638        }
 639
 640        private async ValueTask<Stream> ConnectToTcpHostAsync(string host, int port, HttpRequestMessage initialRequest, 
 0641        {
 0642            cancellationToken.ThrowIfCancellationRequested();
 643
 0644            var endPoint = new DnsEndPoint(host, port);
 0645            Stream? stream = null;
 646            try
 0647            {
 648                // If a ConnectCallback was supplied, use that to establish the connection.
 0649                if (Settings._connectCallback != null)
 0650                {
 0651                    ValueTask<Stream> streamTask = Settings._connectCallback(new SocketsHttpConnectionContext(endPoint, 
 652
 0653                    if (!async && !streamTask.IsCompleted)
 0654                    {
 655                        // User-provided ConnectCallback is completing asynchronously but the user is making a synchrono
 656                        // set it up so that synchronous requests are made on a handler with a synchronously-completing 
 657                        // we could add a Boolean to SocketsHttpConnectionContext (https://github.com/dotnet/runtime/iss
 658                        // this request is sync or async.
 0659                        Trace($"{nameof(SocketsHttpHandler.ConnectCallback)} completing asynchronously for a synchronous
 0660                    }
 661
 0662                    stream = await streamTask.ConfigureAwait(false) ?? throw new HttpRequestException(SR.net_http_null_f
 0663                }
 664                else
 0665                {
 666                    // Otherwise, create and connect a socket using default settings.
 0667                    Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
 668                    try
 0669                    {
 0670                        if (async)
 0671                        {
 0672                            await socket.ConnectAsync(endPoint, cancellationToken).ConfigureAwait(false);
 0673                        }
 674                        else
 0675                        {
 676                            using (cancellationToken.UnsafeRegister(static s => ((Socket)s!).Dispose(), socket))
 0677                            {
 0678                                socket.Connect(endPoint);
 0679                            }
 0680                        }
 681
 0682                        stream = new NetworkStream(socket, ownsSocket: true);
 0683                    }
 0684                    catch
 0685                    {
 0686                        socket.Dispose();
 0687                        throw;
 688                    }
 0689                }
 690
 0691                return stream;
 692            }
 0693            catch (Exception ex)
 0694            {
 0695                throw ex is OperationCanceledException oce && oce.CancellationToken == cancellationToken ?
 0696                    CancellationHelper.CreateOperationCanceledException(innerException: null, cancellationToken) :
 0697                    ConnectHelper.CreateWrappedException(ex, host, port, cancellationToken);
 698            }
 0699        }
 700
 701        private SslClientAuthenticationOptions GetSslOptionsForRequest(HttpRequestMessage request)
 0702        {
 0703            if (_http2Enabled)
 0704            {
 0705                if (request.Version.Major >= 2 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
 0706                {
 0707                    return _sslOptionsHttp2Only!;
 708                }
 709
 0710                if (request.Version.Major >= 2 || request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher)
 0711                {
 0712                    return _sslOptionsHttp2!;
 713                }
 0714            }
 0715            return _sslOptionsHttp11!;
 0716        }
 717
 718        private async ValueTask<Stream> ApplyPlaintextFilterAsync(bool async, Stream stream, Version httpVersion, HttpRe
 0719        {
 0720            if (Settings._plaintextStreamFilter is null)
 0721            {
 0722                return stream;
 723            }
 724
 725            Stream newStream;
 726            try
 0727            {
 0728                ValueTask<Stream> streamTask = Settings._plaintextStreamFilter(new SocketsHttpPlaintextStreamFilterConte
 729
 0730                if (!async && !streamTask.IsCompleted)
 0731                {
 732                    // User-provided PlaintextStreamFilter is completing asynchronously but the user is making a synchro
 733                    // set it up so that synchronous requests are made on a handler with a synchronously-completing Plai
 734                    // we could add a Boolean to SocketsHttpPlaintextStreamFilterContext (https://github.com/dotnet/runt
 735                    // this request is sync or async.
 0736                    Trace($"{nameof(SocketsHttpHandler.PlaintextStreamFilter)} completing asynchronously for a synchrono
 0737                }
 738
 0739                newStream = await streamTask.ConfigureAwait(false);
 0740            }
 0741            catch (OperationCanceledException oce) when (oce.CancellationToken == cancellationToken)
 0742            {
 0743                stream.Dispose();
 0744                throw;
 745            }
 0746            catch (Exception e)
 0747            {
 0748                stream.Dispose();
 0749                throw new HttpRequestException(SR.net_http_exception_during_plaintext_filter, e);
 750            }
 751
 0752            if (newStream == null)
 0753            {
 0754                stream.Dispose();
 0755                throw new HttpRequestException(SR.net_http_null_from_plaintext_filter);
 756            }
 757
 0758            return newStream;
 0759        }
 760
 761        private async ValueTask<Stream> EstablishProxyTunnelAsync(bool async, CancellationToken cancellationToken)
 0762        {
 763            // Send a CONNECT request to the proxy server to establish a tunnel.
 0764            HttpRequestMessage tunnelRequest = new HttpRequestMessage(HttpMethod.Connect, _proxyUri);
 0765            tunnelRequest.Headers.Host = $"{_originAuthority.IdnHost}:{_originAuthority.Port}";    // This specifies des
 766
 0767            if (_connectTunnelUserAgent is not null)
 0768            {
 0769                tunnelRequest.Headers.TryAddWithoutValidation(KnownHeaders.UserAgent.Descriptor, _connectTunnelUserAgent
 0770            }
 771
 0772            HttpResponseMessage tunnelResponse = await _poolManager.SendProxyConnectAsync(tunnelRequest, _proxyUri!, asy
 773
 0774            if (!tunnelResponse.IsSuccessStatusCode)
 0775            {
 0776                tunnelResponse.Dispose();
 0777                throw new HttpRequestException(HttpRequestError.ProxyTunnelError, SR.Format(SR.net_http_proxy_tunnel_ret
 778            }
 779
 780            try
 0781            {
 0782                return tunnelResponse.Content.ReadAsStream(cancellationToken);
 783            }
 0784            catch
 0785            {
 0786                tunnelResponse.Dispose();
 0787                throw;
 788            }
 0789        }
 790
 791        private async ValueTask<Stream> EstablishSocksTunnel(HttpRequestMessage request, bool async, CancellationToken c
 0792        {
 0793            Debug.Assert(_proxyUri != null);
 794
 0795            Stream stream = await ConnectToTcpHostAsync(_proxyUri.IdnHost, _proxyUri.Port, request, async, cancellationT
 796
 797            try
 0798            {
 0799                await SocksHelper.EstablishSocksTunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _pr
 0800            }
 0801            catch (Exception e) when (e is not OperationCanceledException)
 0802            {
 0803                Debug.Assert(e is not HttpRequestException);
 0804                throw new HttpRequestException(HttpRequestError.ProxyTunnelError, SR.net_http_proxy_tunnel_error, e);
 805            }
 806
 0807            return stream;
 0808        }
 809
 810        private CancellationTokenSource GetConnectTimeoutCancellationTokenSource<T>(HttpConnectionWaiter<T> waiter)
 811            where T : HttpConnectionBase?
 0812        {
 0813            var cts = new CancellationTokenSource(Settings._connectTimeout);
 814
 0815            lock (waiter)
 0816            {
 817                // After a request completes (or is canceled), it will call into SetTimeoutToPendingConnectionAttempt,
 818                // which will no-op if ConnectionCancellationTokenSource is not set, assuming that the connection attemp
 819                // As the initiating request for this connection attempt may complete concurrently at any time,
 820                // there is a race condition where the first call to SetTimeoutToPendingConnectionAttempt may happen
 821                // before we were able to set the CTS, so no timeout will be applied even though the request is already 
 0822                waiter.ConnectionCancellationTokenSource = cts;
 823
 824                // To fix that, we check whether the waiter already completed now that we're holding a lock.
 825                // If it had, call SetTimeoutToPendingConnectionAttempt again now that the CTS is set.
 0826                if (waiter.Task.IsCompleted)
 0827                {
 0828                    waiter.SetTimeoutToPendingConnectionAttempt(this, requestCancelled: waiter.Task.IsCanceled);
 0829                    waiter.ConnectionCancellationTokenSource = null;
 0830                }
 0831            }
 832
 0833            return cts;
 0834        }
 835
 836        private static Exception CreateConnectTimeoutException(OperationCanceledException oce)
 0837        {
 838            // The pattern for request timeouts (on HttpClient) is to throw an OCE with an inner exception of TimeoutExc
 839            // Do the same for ConnectTimeout-based timeouts.
 0840            TimeoutException te = new TimeoutException(SR.net_http_connect_timedout, oce.InnerException);
 0841            Exception newException = CancellationHelper.CreateOperationCanceledException(te, oce.CancellationToken);
 0842            ExceptionDispatchInfo.SetCurrentStackTrace(newException);
 0843            return newException;
 0844        }
 845
 846        [DoesNotReturn]
 847        private static void ThrowGetVersionException(HttpRequestMessage request, int desiredVersion, Exception? inner = 
 0848        {
 0849            Debug.Assert(desiredVersion == 2 || desiredVersion == 3);
 850
 0851            HttpRequestException ex = new(HttpRequestError.VersionNegotiationError, SR.Format(SR.net_http_requested_vers
 0852            if (request.IsExtendedConnectRequest && desiredVersion == 2)
 0853            {
 0854                ex.Data["HTTP2_ENABLED"] = false;
 0855            }
 856
 0857            throw ex;
 858        }
 859
 860        private bool CheckExpirationOnGet(HttpConnectionBase connection)
 0861        {
 0862            Debug.Assert(!HasSyncObjLock);
 863
 0864            TimeSpan pooledConnectionLifetime = _poolManager.Settings._pooledConnectionLifetime;
 0865            if (pooledConnectionLifetime != Timeout.InfiniteTimeSpan)
 0866            {
 0867                return connection.GetLifetimeTicks(Environment.TickCount64) > pooledConnectionLifetime.TotalMilliseconds
 868            }
 869
 0870            return false;
 0871        }
 872
 873        private bool CheckExpirationOnReturn(HttpConnectionBase connection)
 0874        {
 0875            TimeSpan lifetime = _poolManager.Settings._pooledConnectionLifetime;
 0876            if (lifetime != Timeout.InfiniteTimeSpan)
 0877            {
 0878                return lifetime == TimeSpan.Zero || connection.GetLifetimeTicks(Environment.TickCount64) > lifetime.Tota
 879            }
 880
 0881            return false;
 0882        }
 883
 884        /// <summary>
 885        /// Disposes the connection pool.  This is only needed when the pool currently contains
 886        /// or has associated connections.
 887        /// </summary>
 888        public void Dispose()
 0889        {
 0890            List<HttpConnectionBase>? toDispose = null;
 891
 0892            lock (SyncObj)
 0893            {
 0894                if (_disposed)
 0895                {
 0896                    return;
 897                }
 898
 0899                _disposed = true;
 0900                _http11RequestQueueIsEmptyAndNotDisposed = false;
 901
 0902                if (NetEventSource.Log.IsEnabled()) Trace("Disposing the pool.");
 903
 0904                if (_availableHttp2Connections is not null)
 0905                {
 0906                    toDispose = [.. _availableHttp2Connections];
 0907                    _associatedHttp2ConnectionCount -= _availableHttp2Connections.Count;
 0908                    _availableHttp2Connections.Clear();
 0909                }
 910
 0911                if (GlobalHttpSettings.SocketsHttpHandler.AllowHttp3 && _availableHttp3Connections is not null)
 0912                {
 0913                    toDispose ??= new();
 0914                    toDispose.AddRange(_availableHttp3Connections);
 0915                    _associatedHttp3ConnectionCount -= _availableHttp3Connections.Count;
 0916                    _availableHttp3Connections.Clear();
 0917                }
 918
 0919                if (_authorityExpireTimer != null)
 0920                {
 0921                    _authorityExpireTimer.Dispose();
 0922                    _authorityExpireTimer = null;
 0923                }
 924
 0925                if (_altSvcBlocklistTimerCancellation != null)
 0926                {
 0927                    _altSvcBlocklistTimerCancellation.Cancel();
 0928                    _altSvcBlocklistTimerCancellation.Dispose();
 0929                    _altSvcBlocklistTimerCancellation = null;
 0930                }
 931
 0932                Debug.Assert((_availableHttp2Connections?.Count ?? 0) == 0, $"Expected {nameof(_availableHttp2Connection
 0933            }
 934
 935            // Dispose connections outside the lock to avoid lock re-entrancy issues.
 936
 937            // This will trigger the disposal of Http11 connections.
 938            // Note: Http11 connections will decrement the _associatedHttp11ConnectionCount when disposed.
 939            // Http2 connections will not, hence the difference in handing _associatedHttp2ConnectionCount.
 0940            ProcessHttp11RequestQueue(null);
 941
 0942            toDispose?.ForEach(c => c.Dispose());
 0943        }
 944
 945        /// <summary>
 946        /// Removes any unusable connections from the pool, and if the pool
 947        /// is then empty and stale, disposes of it.
 948        /// </summary>
 949        /// <returns>
 950        /// true if the pool disposes of itself; otherwise, false.
 951        /// </returns>
 952        public bool CleanCacheAndDisposeIfUnused()
 0953        {
 0954            TimeSpan pooledConnectionLifetime = _poolManager.Settings._pooledConnectionLifetime;
 0955            TimeSpan pooledConnectionIdleTimeout = _poolManager.Settings._pooledConnectionIdleTimeout;
 0956            long nowTicks = Environment.TickCount64;
 957
 0958            List<HttpConnectionBase>? toDispose = null;
 959
 0960            lock (SyncObj)
 0961            {
 962                // If there are now no connections associated with this pool, we can dispose of it. We
 963                // avoid aggressively cleaning up pools that have recently been used but currently aren't;
 964                // if a pool was used since the last time we cleaned up, give it another chance. New pools
 965                // start out saying they've recently been used, to give them a bit of breathing room and time
 966                // for the initial collection to be added to it.
 0967                if (!_usedSinceLastCleanup && _associatedHttp11ConnectionCount == 0 && _associatedHttp2ConnectionCount =
 0968                {
 0969                    _disposed = true;
 0970                    return true; // Pool is disposed of.  It should be removed.
 971                }
 972
 973                // Reset the cleanup flag.  Any pools that are empty and not used since the last cleanup
 974                // will be purged next time around.
 0975                _usedSinceLastCleanup = false;
 976
 0977                ScavengeHttp11ConnectionStack(this, _http11Connections, ref toDispose, nowTicks, pooledConnectionLifetim
 978
 0979                if (_availableHttp2Connections is not null)
 0980                {
 0981                    int removed = ScavengeHttp2ConnectionList(_availableHttp2Connections, ref toDispose, nowTicks, poole
 0982                    _associatedHttp2ConnectionCount -= removed;
 983
 984                    // Note: Http11 connections will decrement the _associatedHttp11ConnectionCount when disposed.
 985                    // Http2 connections will not, hence the difference in handing _associatedHttp2ConnectionCount.
 0986                }
 0987                if (GlobalHttpSettings.SocketsHttpHandler.AllowHttp3 && _availableHttp3Connections is not null)
 0988                {
 0989                    int removed = ScavengeHttp3ConnectionList(_availableHttp3Connections, ref toDispose, nowTicks, poole
 0990                    _associatedHttp3ConnectionCount -= removed;
 991
 992                    // Note: Http11 connections will decrement the _associatedHttp11ConnectionCount when disposed.
 993                    // Http3 connections will not, hence the difference in handing _associatedHttp3ConnectionCount.
 0994                }
 0995            }
 996
 997            // Dispose the stale connections outside the pool lock, to avoid holding the lock too long.
 998            // Dispose them asynchronously to not to block the caller on closing the SslStream or NetworkStream.
 0999            if (toDispose is not null)
 01000            {
 01001                Task.Factory.StartNew(static s => ((List<HttpConnectionBase>)s!).ForEach(c => c.Dispose()), toDispose,
 01002                    CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
 01003            }
 1004
 1005            // Pool is active.  Should not be removed.
 01006            return false;
 01007        }
 1008
 1009        // For diagnostic purposes
 1010        public override string ToString() =>
 01011            $"{nameof(HttpConnectionPool)} " +
 01012            (_proxyUri == null ?
 01013                (_sslOptionsHttp11 == null ?
 01014                    $"http://{_originAuthority}" :
 01015                    $"https://{_originAuthority}" + (_sslOptionsHttp11.TargetHost != _originAuthority.IdnHost ? $", SSL 
 01016                (_sslOptionsHttp11 == null ?
 01017                    $"Proxy {_proxyUri}" :
 01018                    $"https://{_originAuthority}/ tunnelled via Proxy {_proxyUri}" + (_sslOptionsHttp11.TargetHost != _o
 1019
 1020        public void Trace(string? message, [CallerMemberName] string? memberName = null) =>
 01021            NetEventSource.Log.HandlerMessage(
 01022                GetHashCode(),               // pool ID
 01023                0,                           // connection ID
 01024                0,                           // request ID
 01025                memberName,                  // method name
 01026                message);                    // message
 1027    }
 1028}

D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\ConnectionPool\HttpConnectionPool.Http1.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers;
 5using System.Collections.Concurrent;
 6using System.Collections.Generic;
 7using System.Diagnostics;
 8using System.Diagnostics.CodeAnalysis;
 9using System.IO;
 10using System.Threading;
 11using System.Threading.Tasks;
 12
 13namespace System.Net.Http
 14{
 15    internal sealed partial class HttpConnectionPool
 16    {
 17        /// <summary>Stack of currently available HTTP/1.1 connections stored in the pool.</summary>
 018        private readonly ConcurrentStack<HttpConnection> _http11Connections = new();
 19        /// <summary>Controls whether we can use a fast path when returning connections to the pool and skip calling int
 20        private bool _http11RequestQueueIsEmptyAndNotDisposed;
 21        /// <summary>The maximum number of HTTP/1.1 connections allowed to be associated with the pool.</summary>
 22        private readonly int _maxHttp11Connections;
 23        /// <summary>The number of HTTP/1.1 connections associated with the pool, including in use, available, and pendi
 24        private int _associatedHttp11ConnectionCount;
 25        /// <summary>The number of HTTP/1.1 connections that are in the process of being established.</summary>
 26        private int _pendingHttp11ConnectionCount;
 27        /// <summary>Queue of requests waiting for an HTTP/1.1 connection.</summary>
 28        private RequestQueue<HttpConnection> _http11RequestQueue;
 29
 30        /// <summary>For non-proxy connection pools, this is the host name in bytes; for proxies, null.</summary>
 31        private readonly byte[]? _hostHeaderLineBytes;
 32
 033        public byte[]? HostHeaderLineBytes => _hostHeaderLineBytes;
 34
 35        private bool TryGetPooledHttp11Connection(HttpRequestMessage request, bool async, [NotNullWhen(true)] out HttpCo
 036        {
 037            while (_http11Connections.TryPop(out connection))
 038            {
 039                if (CheckExpirationOnGet(connection))
 040                {
 041                    if (NetEventSource.Log.IsEnabled()) connection.Trace("Found expired HTTP/1.1 connection in pool.");
 042                    connection.Dispose();
 043                    continue;
 44                }
 45
 046                if (!connection.PrepareForReuse(async))
 047                {
 048                    if (NetEventSource.Log.IsEnabled()) connection.Trace("Found invalid HTTP/1.1 connection in pool.");
 049                    connection.Dispose();
 050                    continue;
 51                }
 52
 053                if (NetEventSource.Log.IsEnabled()) connection.Trace("Found usable HTTP/1.1 connection in pool.");
 054                waiter = null;
 055                return true;
 56            }
 57
 58            // Slow path - no available connection found.
 59            // Push the request onto the request queue and check if we should inject a new connection.
 60
 061            waiter = new HttpConnectionWaiter<HttpConnection>();
 62
 63            // Technically this block under the lock could be a part of ProcessHttp11RequestQueue to avoid taking the lo
 64            // It is kept separate to simplify that method (avoid extra arguments that are only relevant for this caller
 065            lock (SyncObj)
 066            {
 067                _http11RequestQueue.EnqueueRequest(request, waiter);
 68
 69                // Disable the fast path and force connections returned to the pool to check the request queue first.
 070                _http11RequestQueueIsEmptyAndNotDisposed = false;
 071            }
 72
 73            // Other threads may have added a connection to the pool before we were able to
 74            // add the request to the queue, so we must check for an available connection again.
 75
 076            ProcessHttp11RequestQueue(null);
 077            return false;
 078        }
 79
 80        /// <summary>
 81        /// This method is called:
 82        /// <br/>- When returning a connection and observing that the request queue is not empty (<see cref="_http11Requ
 83        /// <br/>- After adding a request to the queue if we fail to obtain a connection from <see cref="_http11Connecti
 84        /// <br/>- After scavenging or disposing the pool to ensure that any pending requests are handled or connections
 85        /// <para>The method will attempt to match one request from the <see cref="_http11RequestQueue"/> to an availabl
 86        /// The <paramref name="connection"/> can either be provided as an argument (when returning a connection to the 
 87        /// As we'll only process a single request, we are expecting the method to be called every time a request is enq
 88        /// <para>If the <see cref="_http11RequestQueue"/> becomes empty, this method will reset the <see cref="_http11R
 89        /// such that returning connections will use the fast path again and skip calling into this method.</para>
 90        /// <para>Notably, this method will not be called on the fast path as long as we have enough connections to hand
 91        /// </summary>
 92        /// <param name="connection">The connection to use for a pending request, or return to the pool.</param>
 93        private void ProcessHttp11RequestQueue(HttpConnection? connection)
 094        {
 95            // Loop in case the request we try to signal was already cancelled or handled by a different connection.
 096            while (true)
 097            {
 098                HttpConnectionWaiter<HttpConnection>? waiter = null;
 99
 0100                lock (SyncObj)
 0101                {
 102#if DEBUG
 103                    // Other threads may still interact with the connections stack. Read the count once to keep the asse
 0104                    int connectionCount = _http11Connections.Count;
 0105                    Debug.Assert(_associatedHttp11ConnectionCount >= connectionCount + _pendingHttp11ConnectionCount,
 0106                        $"Expected {_associatedHttp11ConnectionCount} >= {connectionCount} + {_pendingHttp11ConnectionCo
 107#endif
 0108                    Debug.Assert(_associatedHttp11ConnectionCount <= _maxHttp11Connections,
 0109                        $"Expected {_associatedHttp11ConnectionCount} <= {_maxHttp11Connections}");
 0110                    Debug.Assert(_associatedHttp11ConnectionCount >= _pendingHttp11ConnectionCount,
 0111                        $"Expected {_associatedHttp11ConnectionCount} >= {_pendingHttp11ConnectionCount}");
 112
 0113                    if (_http11RequestQueue.Count != 0)
 0114                    {
 0115                        if (connection is not null || _http11Connections.TryPop(out connection))
 0116                        {
 117                            // If the connection is new, this check will always succeed as there is no scavenging task p
 0118                            if (!connection.TryOwnScavengingTaskCompletion())
 0119                            {
 0120                                goto DisposeConnection;
 121                            }
 122
 123                            // TryDequeueWaiter will prune completed requests from the head of the queue,
 124                            // so it's possible for it to return false even though we checked that Count != 0.
 0125                            bool success = _http11RequestQueue.TryDequeueWaiter(this, out waiter);
 0126                            Debug.Assert(success == waiter is not null);
 0127                        }
 0128                    }
 129
 130                    // Update the empty queue flag now.
 131                    // If the request queue is now empty, returning connections will use the fast path and skip calling 
 0132                    _http11RequestQueueIsEmptyAndNotDisposed = _http11RequestQueue.Count == 0 && !_disposed;
 133
 0134                    if (waiter is null)
 0135                    {
 136                        // We didn't find a waiter to signal, or there were no connections available.
 137
 0138                        if (connection is not null)
 0139                        {
 140                            // A connection was provided to this method, or we rented one from the pool.
 141                            // Return it back to the pool since we're not going to use it yet.
 142
 143                            // We're returning it while holding the lock to avoid a scenario where
 144                            // - thread A sees no requests are waiting in the queue (current thread)
 145                            // - thread B adds a request to the queue, and sees no connections are available
 146                            // - thread A returns the connection to the pool
 147                            // We'd have both a connection and a request waiting in the pool, but nothing to pair the tw
 148
 149                            // The main scenario where we'll reach this branch is when we enqueue a request to the queue
 150                            // and set the _http11RequestQueueIsEmptyAndNotDisposed flag to false, followed by multiple
 151                            // returning connections observing the flag and calling into this method before we clear the
 152                            // This should be a relatively rare case, so the added contention should be minimal.
 153
 154                            // We took ownership of the scavenging task completion.
 155                            // If we can't return the completion (the task already completed), we must dispose the conne
 0156                            if (!connection.TryReturnScavengingTaskCompletionOwnership())
 0157                            {
 0158                                goto DisposeConnection;
 159                            }
 160
 0161                            _http11Connections.Push(connection);
 0162                        }
 163                        else
 0164                        {
 165                            // We may be out of available connections, check if we should inject a new one.
 0166                            CheckForHttp11ConnectionInjection();
 0167                        }
 168
 0169                        break;
 170                    }
 0171                }
 172
 0173                Debug.Assert(connection is not null);
 174
 0175                if (waiter.TrySignal(connection))
 0176                {
 177                    // Success. Note that we did not call connection.PrepareForReuse
 178                    // before signaling the waiter. This is intentional, as the fact that
 179                    // this method was called indicates that the connection is either new,
 180                    // or was just returned to the pool and is still in a good state.
 181                    //
 182                    // We must, however, take ownership of the scavenging task completion as
 183                    // there is a small chance that such a task was started if the connection
 184                    // was briefly returned to the pool.
 0185                    return;
 186                }
 187
 188                // The request was already cancelled or handled by a different connection.
 189
 190                // We took ownership of the scavenging task completion.
 191                // If we can't return the completion (the task already completed), we must dispose the connection.
 0192                if (!connection.TryReturnScavengingTaskCompletionOwnership())
 0193                {
 0194                    goto DisposeConnection;
 195                }
 196
 197                // Loop again to try to find another request to signal, or return the connection.
 0198                continue;
 199
 0200            DisposeConnection:
 201                // The scavenging task completed before we assigned a request to the connection.
 202                // We've received EOF/erroneous data and the connection is not usable anymore.
 203                // Throw it away and try again.
 0204                connection.Dispose();
 0205                connection = null;
 0206            }
 207
 0208            if (_disposed)
 0209            {
 210                // The pool is being disposed and there are no more requests to handle.
 211                // Clean up any idle connections still waiting in the pool.
 0212                while (_http11Connections.TryPop(out connection))
 0213                {
 0214                    connection.Dispose();
 0215                }
 0216            }
 0217        }
 218
 219        private void CheckForHttp11ConnectionInjection()
 0220        {
 0221            Debug.Assert(HasSyncObjLock);
 222
 0223            _http11RequestQueue.PruneCompletedRequestsFromHeadOfQueue(this);
 224
 225            // Determine if we can and should add a new connection to the pool.
 0226            bool willInject =
 0227                _http11RequestQueue.Count > _pendingHttp11ConnectionCount &&    // More requests queued than pending con
 0228                _associatedHttp11ConnectionCount < _maxHttp11Connections &&     // Under the connection limit
 0229                _http11RequestQueue.RequestsWithoutAConnectionAttempt > 0;      // There are requests we haven't issued 
 230
 0231            if (NetEventSource.Log.IsEnabled())
 0232            {
 0233                Trace($"Available HTTP/1.1 connections: {_http11Connections.Count}, Requests in the queue: {_http11Reque
 0234                    $"Requests without a connection attempt: {_http11RequestQueue.RequestsWithoutAConnectionAttempt}, " 
 0235                    $"Pending HTTP/1.1 connections: {_pendingHttp11ConnectionCount}, Total associated HTTP/1.1 connectio
 0236                    $"Max HTTP/1.1 connection limit: {_maxHttp11Connections}, " +
 0237                    $"Will inject connection: {willInject}.");
 0238            }
 239
 0240            if (willInject)
 0241            {
 0242                _associatedHttp11ConnectionCount++;
 0243                _pendingHttp11ConnectionCount++;
 244
 0245                RequestQueue<HttpConnection>.QueueItem queueItem = _http11RequestQueue.PeekNextRequestForConnectionAttem
 0246                _ = InjectNewHttp11ConnectionAsync(queueItem); // ignore returned task
 0247            }
 0248        }
 249
 250        private async Task InjectNewHttp11ConnectionAsync(RequestQueue<HttpConnection>.QueueItem queueItem)
 0251        {
 0252            if (NetEventSource.Log.IsEnabled()) Trace("Creating new HTTP/1.1 connection for pool.");
 253
 254            // Queue the remainder of the work so that this method completes quickly
 255            // and escapes locks held by the caller.
 0256            await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
 257
 0258            HttpConnectionWaiter<HttpConnection> waiter = queueItem.Waiter;
 0259            HttpConnection? connection = null;
 0260            Exception? connectionException = null;
 261
 0262            CancellationTokenSource cts = GetConnectTimeoutCancellationTokenSource(waiter);
 263            try
 0264            {
 0265                connection = await CreateHttp11ConnectionAsync(queueItem.Request, true, cts.Token).ConfigureAwait(false)
 0266            }
 0267            catch (Exception e)
 0268            {
 0269                connectionException = e is OperationCanceledException oce && oce.CancellationToken == cts.Token && !wait
 0270                    CreateConnectTimeoutException(oce) :
 0271                    e;
 0272            }
 273            finally
 0274            {
 0275                lock (waiter)
 0276                {
 0277                    waiter.ConnectionCancellationTokenSource = null;
 0278                    cts.Dispose();
 0279                }
 0280            }
 281
 0282            if (connection is not null)
 0283            {
 284                // Add the established connection to the pool.
 0285                AddNewHttp11Connection(connection, queueItem.Waiter);
 0286            }
 287            else
 0288            {
 0289                Debug.Assert(connectionException is not null);
 0290                HandleHttp11ConnectionFailure(waiter, connectionException);
 0291            }
 0292        }
 293
 294        internal async ValueTask<HttpConnection> CreateHttp11ConnectionAsync(HttpRequestMessage request, bool async, Can
 0295        {
 0296            (Stream stream, TransportContext? transportContext, Activity? activity,  IPEndPoint? remoteEndPoint) = await
 0297            return await ConstructHttp11ConnectionAsync(async, stream, transportContext, request, activity, remoteEndPoi
 0298        }
 299
 300        private async ValueTask<HttpConnection> ConstructHttp11ConnectionAsync(bool async, Stream stream, TransportConte
 0301        {
 0302            Stream newStream = await ApplyPlaintextFilterAsync(async, stream, HttpVersion.Version11, request, cancellati
 0303            return new HttpConnection(this, newStream, transportContext, activity, remoteEndPoint);
 0304        }
 305
 306        private void HandleHttp11ConnectionFailure(HttpConnectionWaiter<HttpConnection>? requestWaiter, Exception e)
 0307        {
 0308            if (NetEventSource.Log.IsEnabled()) Trace($"HTTP/1.1 connection failed: {e}");
 309
 310            // If this is happening as part of an HTTP/2 => HTTP/1.1 downgrade, we won't have an HTTP/1.1 waiter associa
 311            // We don't care if this fails; that means the request was previously canceled or handled by a different con
 0312            requestWaiter?.TrySetException(e);
 313
 0314            lock (SyncObj)
 0315            {
 0316                Debug.Assert(_associatedHttp11ConnectionCount > 0);
 0317                Debug.Assert(_pendingHttp11ConnectionCount > 0);
 318
 0319                _associatedHttp11ConnectionCount--;
 0320                _pendingHttp11ConnectionCount--;
 321
 0322                CheckForHttp11ConnectionInjection();
 0323            }
 0324        }
 325
 326        public void RecycleHttp11Connection(HttpConnection connection)
 0327        {
 0328            if (CheckExpirationOnReturn(connection))
 0329            {
 0330                if (NetEventSource.Log.IsEnabled()) connection.Trace("Disposing HTTP/1.1 connection when returning to po
 0331                connection.Dispose();
 0332                return;
 333            }
 334
 0335            ReturnHttp11Connection(connection);
 0336        }
 337
 338        private void AddNewHttp11Connection(HttpConnection connection, HttpConnectionWaiter<HttpConnection>? initialRequ
 0339        {
 0340            if (NetEventSource.Log.IsEnabled()) Trace("");
 341
 0342            lock (SyncObj)
 0343            {
 0344                Debug.Assert(_pendingHttp11ConnectionCount > 0);
 0345                _pendingHttp11ConnectionCount--;
 346
 0347                if (initialRequestWaiter is not null)
 0348                {
 349                    // If we're about to signal the initial waiter, that request must be removed from the queue if it wa
 350                    // Normally, TryDequeueWaiter would handle the removal. TryDequeueSpecificWaiter matches this behavi
 351                    // We don't care if this fails; that means the request was previously canceled, handled by a differe
 0352                    _http11RequestQueue.TryDequeueSpecificWaiter(initialRequestWaiter);
 353
 354                    // There's no need for us to hold the lock while signaling the waiter.
 0355                }
 0356            }
 357
 0358            if (initialRequestWaiter is not null &&
 0359                initialRequestWaiter.TrySignal(connection))
 0360            {
 0361                return;
 362            }
 363
 0364            ReturnHttp11Connection(connection);
 0365        }
 366
 367        private void ReturnHttp11Connection(HttpConnection connection)
 0368        {
 0369            connection.MarkConnectionAsIdle();
 370
 371            // The fast path when there are enough connections and no pending requests
 372            // is that we'll see _http11RequestQueueIsEmptyAndNotDisposed being true both
 373            // times, and all we'll have to do as part of returning the connection is
 374            // a Push call on the concurrent stack.
 375
 0376            if (Volatile.Read(ref _http11RequestQueueIsEmptyAndNotDisposed))
 0377            {
 0378                _http11Connections.Push(connection);
 379
 380                // When we add a connection to the pool, we must ensure that there are
 381                // either no pending requests waiting, or that _something_ will pair those
 382                // requests with the connection we just added.
 383
 384                // When adding a request to the queue, we'll first check if there's
 385                // an available connection waiting in the pool that we could use.
 386                // If there isn't, we'll set the _http11RequestQueueIsEmptyAndNotDisposed
 387                // flag and check for available connections again.
 388
 389                // To avoid a race where we add the connection after a request was enqueued,
 390                // we'll check the flag again and try to process one request from the queue.
 391
 0392                if (!Volatile.Read(ref _http11RequestQueueIsEmptyAndNotDisposed))
 0393                {
 0394                    ProcessHttp11RequestQueue(null);
 0395                }
 0396            }
 397            else
 0398            {
 399                // ProcessHttp11RequestQueue is responsible for handing the connection to a pending request,
 400                // or to return it back to the pool if there aren't any.
 401
 402                // We hand over the connection directly instead of pushing it on the stack first to ensure
 403                // that pending requests are processed in a fair (FIFO) order.
 0404                ProcessHttp11RequestQueue(connection);
 0405            }
 0406        }
 407
 408        /// <summary>
 409        /// Called when an HttpConnection from this pool is no longer usable.
 410        /// Note, this is always called from HttpConnection.Dispose, which is a bit different than how HTTP2 works.
 411        /// </summary>
 412        public void InvalidateHttp11Connection(HttpConnection connection, bool disposing = true)
 0413        {
 0414            lock (SyncObj)
 0415            {
 0416                Debug.Assert(_associatedHttp11ConnectionCount > 0);
 0417                Debug.Assert(!disposing || Array.IndexOf(_http11Connections.ToArray(), connection) < 0);
 418
 0419                _associatedHttp11ConnectionCount--;
 420
 0421                CheckForHttp11ConnectionInjection();
 0422            }
 0423        }
 424
 425        private static void ScavengeHttp11ConnectionStack(HttpConnectionPool pool, ConcurrentStack<HttpConnection> conne
 0426        {
 427            // We can't simply enumerate the connections stack as other threads may still be adding and removing entries
 428            // If we want to check the state of a connection, we must take it from the stack first to ensure we own it.
 429
 430            // We're about to starve the connection pool of all available connections for a moment.
 431            // We must be holding the lock while doing so to ensure that any new requests that
 432            // come in during this time will be blocked waiting in ProcessHttp11RequestQueue.
 433            // If this were not the case, requests would repeatedly call into CheckForHttp11ConnectionInjection
 434            // and trigger new connection attempts, even if we have enough connections in our copy.
 0435            Debug.Assert(pool.HasSyncObjLock);
 0436            Debug.Assert(connections.Count <= pool._associatedHttp11ConnectionCount);
 437
 0438            HttpConnection[] stackCopy = ArrayPool<HttpConnection>.Shared.Rent(pool._associatedHttp11ConnectionCount);
 0439            int usableConnections = 0;
 440
 0441            while (connections.TryPop(out HttpConnection? connection))
 0442            {
 0443                if (connection.IsUsable(nowTicks, pooledConnectionLifetime, pooledConnectionIdleTimeout))
 0444                {
 0445                    stackCopy[usableConnections++] = connection;
 0446                }
 447                else
 0448                {
 0449                    toDispose ??= new List<HttpConnectionBase>();
 0450                    toDispose.Add(connection);
 0451                }
 0452            }
 453
 0454            if (usableConnections > 0)
 0455            {
 456                // Add them back in reverse to maintain the LIFO order.
 0457                Span<HttpConnection> usable = stackCopy.AsSpan(0, usableConnections);
 0458                usable.Reverse();
 0459                connections.PushRange(stackCopy, 0, usableConnections);
 0460                usable.Clear();
 0461            }
 462
 0463            ArrayPool<HttpConnection>.Shared.Return(stackCopy);
 0464        }
 465    }
 466}

D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\ConnectionPool\HttpConnectionPool.Http2.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Collections.Generic;
 5using System.Diagnostics;
 6using System.Diagnostics.CodeAnalysis;
 7using System.Globalization;
 8using System.IO;
 9using System.Net.Security;
 10using System.Runtime.ExceptionServices;
 11using System.Security.Authentication;
 12using System.Text;
 13using System.Threading;
 14using System.Threading.Tasks;
 15
 16namespace System.Net.Http
 17{
 18    internal sealed partial class HttpConnectionPool
 19    {
 20        /// <summary>List of available HTTP/2 connections stored in the pool.</summary>
 21        private List<Http2Connection>? _availableHttp2Connections;
 22        /// <summary>The number of HTTP/2 connections associated with the pool, including in use, available, and pending
 23        private int _associatedHttp2ConnectionCount;
 24        /// <summary>Indicates whether an HTTP/2 connection is in the process of being established.</summary>
 25        private bool _pendingHttp2Connection;
 26        /// <summary>Queue of requests waiting for an HTTP/2 connection.</summary>
 27        private RequestQueue<Http2Connection?> _http2RequestQueue;
 28
 29        private bool _http2Enabled;
 30        private byte[]? _http2AltSvcOriginUri;
 31        internal readonly byte[]? _http2EncodedAuthorityHostHeader;
 32
 33        /// <summary>
 34        /// An ASCII origin string per RFC 6454 Section 6.2, in format &lt;scheme&gt;://&lt;host&gt;[:&lt;port&gt;]
 35        /// </summary>
 36        /// <remarks>
 37        /// Used by <see cref="Http2Connection"/> to test ALTSVC frames for our origin.
 38        /// </remarks>
 39        public byte[] Http2AltSvcOriginUri
 40        {
 41            get
 042            {
 043                if (_http2AltSvcOriginUri == null)
 044                {
 045                    var sb = new StringBuilder();
 46
 047                    sb.Append(IsSecure ? "https://" : "http://")
 048                      .Append(_originAuthority.IdnHost);
 49
 050                    if (!IsDefaultPort)
 051                    {
 052                        sb.Append(CultureInfo.InvariantCulture, $":{_originAuthority.Port}");
 053                    }
 54
 055                    _http2AltSvcOriginUri = Encoding.ASCII.GetBytes(sb.ToString());
 056                }
 57
 058                return _http2AltSvcOriginUri;
 059            }
 60        }
 61
 062        private bool EnableMultipleHttp2Connections => _poolManager.Settings.EnableMultipleHttp2Connections;
 63
 64        private bool TryGetPooledHttp2Connection(HttpRequestMessage request, [NotNullWhen(true)] out Http2Connection? co
 065        {
 066            Debug.Assert(_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == Htt
 67
 68            // Look for a usable connection.
 069            while (true)
 070            {
 071                lock (SyncObj)
 072                {
 073                    if (!_http2Enabled)
 074                    {
 075                        waiter = null;
 076                        connection = null;
 077                        return false;
 78                    }
 79
 080                    int availableConnectionCount = _availableHttp2Connections?.Count ?? 0;
 081                    if (availableConnectionCount > 0)
 082                    {
 83                        // We have a connection that we can attempt to use.
 84                        // Validate it below outside the lock, to avoid doing expensive operations while holding the loc
 085                        connection = _availableHttp2Connections![availableConnectionCount - 1];
 086                    }
 87                    else
 088                    {
 89                        // No available connections. Add to the request queue.
 090                        waiter = _http2RequestQueue.EnqueueRequest(request);
 91
 092                        CheckForHttp2ConnectionInjection();
 93
 94                        // There were no available connections. This request has been added to the request queue.
 095                        if (NetEventSource.Log.IsEnabled()) Trace($"No available HTTP/2 connections; request queued.");
 096                        connection = null;
 097                        return false;
 98                    }
 099                }
 100
 0101                if (CheckExpirationOnGet(connection))
 0102                {
 0103                    if (NetEventSource.Log.IsEnabled()) connection.Trace("Found expired HTTP/2 connection in pool.");
 104
 0105                    InvalidateHttp2Connection(connection);
 0106                    continue;
 107                }
 108
 0109                if (!connection.TryReserveStream())
 0110                {
 0111                    if (NetEventSource.Log.IsEnabled()) connection.Trace("Found HTTP/2 connection in pool without availa
 112
 0113                    bool found = false;
 0114                    lock (SyncObj)
 0115                    {
 0116                        int index = _availableHttp2Connections.IndexOf(connection);
 0117                        if (index != -1)
 0118                        {
 0119                            found = true;
 0120                            _availableHttp2Connections.RemoveAt(index);
 0121                        }
 0122                    }
 123
 124                    // If we didn't find the connection, then someone beat us to removing it (or it shut down)
 0125                    if (found)
 0126                    {
 0127                        DisableHttp2Connection(connection);
 0128                    }
 0129                    continue;
 130                }
 131
 0132                if (NetEventSource.Log.IsEnabled()) connection.Trace("Found usable HTTP/2 connection in pool.");
 0133                waiter = null;
 0134                return true;
 135            }
 0136        }
 137
 138        private void CheckForHttp2ConnectionInjection()
 0139        {
 0140            Debug.Assert(HasSyncObjLock);
 141
 0142            _http2RequestQueue.PruneCompletedRequestsFromHeadOfQueue(this);
 143
 144            // Determine if we can and should add a new connection to the pool.
 0145            int availableHttp2ConnectionCount = _availableHttp2Connections?.Count ?? 0;
 0146            bool willInject = availableHttp2ConnectionCount == 0 &&                         // No available connections
 0147                !_pendingHttp2Connection &&                                                 // Only allow one pending HT
 0148                _http2RequestQueue.Count > 0 &&                                             // There are requests left o
 0149                (_associatedHttp2ConnectionCount == 0 || EnableMultipleHttp2Connections) && // We allow multiple connect
 0150                _http2RequestQueue.RequestsWithoutAConnectionAttempt > 0;                   // There are requests we hav
 151
 0152            if (NetEventSource.Log.IsEnabled())
 0153            {
 0154                Trace($"Available HTTP/2.0 connections: {availableHttp2ConnectionCount}, " +
 0155                    $"Pending HTTP/2.0 connection: {_pendingHttp2Connection}, " +
 0156                    $"Requests in the queue: {_http2RequestQueue.Count}, " +
 0157                    $"Requests without a connection attempt: {_http2RequestQueue.RequestsWithoutAConnectionAttempt}, " +
 0158                    $"Total associated HTTP/2.0 connections: {_associatedHttp2ConnectionCount}, " +
 0159                    $"Will inject connection: {willInject}.");
 0160            }
 161
 0162            if (willInject)
 0163            {
 0164                _associatedHttp2ConnectionCount++;
 0165                _pendingHttp2Connection = true;
 166
 0167                RequestQueue<Http2Connection?>.QueueItem queueItem = _http2RequestQueue.PeekNextRequestForConnectionAtte
 0168                _ = InjectNewHttp2ConnectionAsync(queueItem); // ignore returned task
 0169            }
 0170        }
 171
 172        private async Task InjectNewHttp2ConnectionAsync(RequestQueue<Http2Connection?>.QueueItem queueItem)
 0173        {
 0174            if (NetEventSource.Log.IsEnabled()) Trace("Creating new HTTP/2 connection for pool.");
 175
 176            // Queue the remainder of the work so that this method completes quickly
 177            // and escapes locks held by the caller.
 0178            await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
 179
 0180            Http2Connection? connection = null;
 0181            Exception? connectionException = null;
 0182            HttpConnectionWaiter<Http2Connection?> waiter = queueItem.Waiter;
 183
 0184            CancellationTokenSource cts = GetConnectTimeoutCancellationTokenSource(waiter);
 185            try
 0186            {
 0187                (Stream stream, TransportContext? transportContext, Activity? activity, IPEndPoint? remoteEndPoint) = aw
 188
 0189                if (IsSecure)
 0190                {
 0191                    SslStream sslStream = (SslStream)stream;
 192
 0193                    if (sslStream.NegotiatedApplicationProtocol == SslApplicationProtocol.Http2)
 0194                    {
 195                        // The server accepted our request for HTTP2.
 196
 0197                        if (sslStream.SslProtocol < SslProtocols.Tls12)
 0198                        {
 0199                            stream.Dispose();
 0200                            connectionException = new HttpRequestException(SR.Format(SR.net_ssl_http2_requires_tls12, ss
 0201                        }
 202                        else
 0203                        {
 0204                            connection = await ConstructHttp2ConnectionAsync(stream, queueItem.Request, activity, remote
 0205                        }
 0206                    }
 207                    else
 0208                    {
 209                        // We established an SSL connection, but the server denied our request for HTTP2.
 0210                        await HandleHttp11Downgrade(queueItem.Request, stream, transportContext, activity, remoteEndPoin
 0211                        return;
 212                    }
 0213                }
 214                else
 0215                {
 0216                    connection = await ConstructHttp2ConnectionAsync(stream, queueItem.Request, activity, remoteEndPoint
 0217                }
 0218            }
 0219            catch (Exception e)
 0220            {
 0221                connectionException = e is OperationCanceledException oce && oce.CancellationToken == cts.Token && !wait
 0222                    CreateConnectTimeoutException(oce) :
 0223                    e;
 0224            }
 225            finally
 0226            {
 0227                lock (waiter)
 0228                {
 0229                    waiter.ConnectionCancellationTokenSource = null;
 0230                    cts.Dispose();
 0231                }
 0232            }
 233
 0234            if (connection is not null)
 0235            {
 236                // Add the new connection to the pool.
 0237                ReturnHttp2Connection(connection, isNewConnection: true, queueItem.Waiter);
 0238            }
 239            else
 0240            {
 0241                Debug.Assert(connectionException is not null);
 0242                HandleHttp2ConnectionFailure(waiter, connectionException);
 0243            }
 0244        }
 245
 246        private async ValueTask<Http2Connection> ConstructHttp2ConnectionAsync(Stream stream, HttpRequestMessage request
 0247        {
 0248            stream = await ApplyPlaintextFilterAsync(async: true, stream, HttpVersion.Version20, request, cancellationTo
 249
 0250            Http2Connection http2Connection = new Http2Connection(this, stream, activity, remoteEndPoint);
 251            try
 0252            {
 0253                await http2Connection.SetupAsync(cancellationToken).ConfigureAwait(false);
 0254            }
 0255            catch (Exception e)
 0256            {
 257                // Note, SetupAsync will dispose the connection if there is an exception.
 0258                if (e is OperationCanceledException oce && oce.CancellationToken == cancellationToken)
 0259                {
 260                    // Note, AddHttp2ConnectionAsync handles this OCE separately so don't wrap it.
 0261                    throw;
 262                }
 263
 0264                throw new HttpRequestException(SR.net_http_client_execution_error, e);
 265            }
 266
 0267            return http2Connection;
 0268        }
 269
 270        private void HandleHttp2ConnectionFailure(HttpConnectionWaiter<Http2Connection?> requestWaiter, Exception e)
 0271        {
 0272            if (NetEventSource.Log.IsEnabled()) Trace($"HTTP2 connection failed: {e}");
 273
 274            // We don't care if this fails; that means the request was previously canceled or handled by a different con
 0275            requestWaiter.TrySetException(e);
 276
 0277            lock (SyncObj)
 0278            {
 0279                Debug.Assert(_associatedHttp2ConnectionCount > 0);
 0280                Debug.Assert(_pendingHttp2Connection);
 281
 0282                _associatedHttp2ConnectionCount--;
 0283                _pendingHttp2Connection = false;
 284
 0285                CheckForHttp2ConnectionInjection();
 0286            }
 0287        }
 288
 289        private async Task HandleHttp11Downgrade(HttpRequestMessage request, Stream stream, TransportContext? transportC
 0290        {
 0291            if (NetEventSource.Log.IsEnabled()) Trace("Server does not support HTTP2; disabling HTTP2 use and proceeding
 292
 0293            bool canUse = true;
 0294            HttpConnectionWaiter<Http2Connection?>? waiter = null;
 0295            lock (SyncObj)
 0296            {
 0297                Debug.Assert(_pendingHttp2Connection);
 0298                Debug.Assert(_associatedHttp2ConnectionCount > 0);
 299
 300                // Server does not support HTTP2. Disable further HTTP2 attempts.
 0301                _http2Enabled = false;
 0302                _associatedHttp2ConnectionCount--;
 0303                _pendingHttp2Connection = false;
 304
 0305                if (_associatedHttp11ConnectionCount < _maxHttp11Connections)
 0306                {
 0307                    _associatedHttp11ConnectionCount++;
 0308                    _pendingHttp11ConnectionCount++;
 0309                }
 310                else
 0311                {
 312                    // We are already at the limit for HTTP/1.1 connections, so do not proceed with this connection.
 0313                    canUse = false;
 0314                }
 315
 0316                _http2RequestQueue.TryDequeueWaiter(this, out waiter);
 0317            }
 318
 319            // Signal to any queued HTTP2 requests that they must downgrade.
 0320            while (waiter is not null)
 0321            {
 0322                if (NetEventSource.Log.IsEnabled()) Trace("Downgrading queued HTTP2 request to HTTP/1.1");
 323
 324                // We are done with the HTTP2 connection attempt, no point to cancel it.
 0325                Volatile.Write(ref waiter.ConnectionCancellationTokenSource, null);
 326
 327                // We don't care if this fails; that means the request was previously canceled or handled by a different
 0328                waiter.TrySetResult(null);
 329
 0330                lock (SyncObj)
 0331                {
 0332                    _http2RequestQueue.TryDequeueWaiter(this, out waiter);
 0333                }
 0334            }
 335
 0336            if (!canUse)
 0337            {
 0338                if (NetEventSource.Log.IsEnabled()) Trace("Discarding downgraded HTTP/1.1 connection because HTTP/1.1 co
 0339                stream.Dispose();
 0340                return;
 341            }
 342
 343            HttpConnection http11Connection;
 344            try
 0345            {
 346                // Note, the same CancellationToken from the original HTTP2 connection establishment still applies here.
 0347                http11Connection = await ConstructHttp11ConnectionAsync(true, stream, transportContext, request, activit
 0348            }
 0349            catch (OperationCanceledException oce) when (oce.CancellationToken == cancellationToken)
 0350            {
 0351                HandleHttp11ConnectionFailure(requestWaiter: null, CreateConnectTimeoutException(oce));
 0352                return;
 353            }
 0354            catch (Exception e)
 0355            {
 0356                HandleHttp11ConnectionFailure(requestWaiter: null, e);
 0357                return;
 358            }
 359
 0360            AddNewHttp11Connection(http11Connection, initialRequestWaiter: null);
 0361        }
 362
 363        private void ReturnHttp2Connection(Http2Connection connection, bool isNewConnection, HttpConnectionWaiter<Http2C
 0364        {
 0365            if (NetEventSource.Log.IsEnabled()) connection.Trace($"{nameof(isNewConnection)}={isNewConnection}");
 366
 0367            Debug.Assert(!HasSyncObjLock);
 0368            Debug.Assert(isNewConnection || initialRequestWaiter is null, "Shouldn't have a request unless the connectio
 369
 0370            if (!isNewConnection && CheckExpirationOnReturn(connection))
 0371            {
 0372                lock (SyncObj)
 0373                {
 0374                    Debug.Assert(_availableHttp2Connections is null || !_availableHttp2Connections.Contains(connection))
 0375                    Debug.Assert(_associatedHttp2ConnectionCount > (_availableHttp2Connections?.Count ?? 0));
 0376                    _associatedHttp2ConnectionCount--;
 0377                }
 378
 0379                if (NetEventSource.Log.IsEnabled()) connection.Trace("Disposing HTTP2 connection return to pool. Connect
 0380                connection.Dispose();
 0381                return;
 382            }
 383
 0384            while (connection.TryReserveStream())
 0385            {
 386                // Loop in case we get a request that has already been canceled or handled by a different connection.
 0387                while (true)
 0388                {
 0389                    HttpConnectionWaiter<Http2Connection?>? waiter = null;
 0390                    bool added = false;
 0391                    lock (SyncObj)
 0392                    {
 0393                        Debug.Assert(_availableHttp2Connections is null || !_availableHttp2Connections.Contains(connecti
 0394                        Debug.Assert(_associatedHttp2ConnectionCount > (_availableHttp2Connections?.Count ?? 0),
 0395                            $"Expected _associatedHttp2ConnectionCount={_associatedHttp2ConnectionCount} > _availableHtt
 396
 0397                        if (isNewConnection)
 0398                        {
 0399                            Debug.Assert(_pendingHttp2Connection);
 0400                            _pendingHttp2Connection = false;
 0401                            isNewConnection = false;
 0402                        }
 403
 0404                        if (initialRequestWaiter is not null)
 0405                        {
 406                            // Try to handle the request that we initiated the connection for first
 0407                            waiter = initialRequestWaiter;
 0408                            initialRequestWaiter = null;
 409
 410                            // If this method found a request to service, that request must be removed from the queue if
 411                            // Normally, TryDequeueWaiter would handle the removal. TryDequeueSpecificWaiter matches thi
 412                            // We don't care if this fails; that means the request was previously canceled, handled by a
 0413                            _http2RequestQueue.TryDequeueSpecificWaiter(waiter);
 0414                        }
 0415                        else if (_http2RequestQueue.TryDequeueWaiter(this, out waiter))
 0416                        {
 0417                            Debug.Assert((_availableHttp2Connections?.Count ?? 0) == 0, $"With {(_availableHttp2Connecti
 0418                        }
 0419                        else if (_disposed)
 0420                        {
 421                            // The pool has been disposed. We will dispose this connection below outside the lock.
 422                            // We do this check after processing the request queue so that any queued requests will be h
 0423                            _associatedHttp2ConnectionCount--;
 0424                        }
 425                        else
 0426                        {
 427                            // Add connection to the pool.
 0428                            added = true;
 0429                            _availableHttp2Connections ??= new List<Http2Connection>();
 0430                            _availableHttp2Connections.Add(connection);
 0431                        }
 0432                    }
 433
 0434                    if (waiter is not null)
 0435                    {
 0436                        Debug.Assert(!added);
 437
 0438                        if (waiter.TrySignal(connection))
 0439                        {
 0440                            break;
 441                        }
 442
 443                        // Loop and process the queue again
 0444                    }
 445                    else
 0446                    {
 0447                        connection.ReleaseStream();
 0448                        if (added)
 0449                        {
 0450                            if (NetEventSource.Log.IsEnabled()) connection.Trace("Put HTTP2 connection in pool.");
 0451                            return;
 452                        }
 453                        else
 0454                        {
 0455                            Debug.Assert(_disposed);
 0456                            if (NetEventSource.Log.IsEnabled()) connection.Trace("Disposing HTTP2 connection returned to
 0457                            connection.Dispose();
 0458                            return;
 459                        }
 460                    }
 0461                }
 0462            }
 463
 0464            if (isNewConnection)
 0465            {
 0466                Debug.Assert(initialRequestWaiter is not null, "Expect request for a new connection");
 467
 468                // The new connection could not handle even one request, either because it shut down before we could use
 469                // or because it immediately set the max concurrent streams limit to 0.
 470                // We don't want to get stuck in a loop where we keep trying to create new connections for the same requ
 471                // So, treat this as a connection failure.
 472
 0473                if (NetEventSource.Log.IsEnabled()) connection.Trace("New HTTP2 connection is unusable due to no availab
 0474                connection.Dispose();
 475
 0476                HttpRequestException hre = new HttpRequestException(SR.net_http_http2_connection_not_established);
 0477                ExceptionDispatchInfo.SetCurrentStackTrace(hre);
 0478                HandleHttp2ConnectionFailure(initialRequestWaiter, hre);
 0479            }
 480            else
 0481            {
 482                // Since we only inject one connection at a time, we may want to inject another now.
 0483                lock (SyncObj)
 0484                {
 0485                    CheckForHttp2ConnectionInjection();
 0486                }
 487
 488                // We need to wait until the connection is usable again.
 0489                DisableHttp2Connection(connection);
 0490            }
 0491        }
 492
 493        /// <summary>
 494        /// Disable usage of the specified connection because it cannot handle any more streams at the moment.
 495        /// We will register to be notified when it can handle more streams (or becomes permanently unusable).
 496        /// </summary>
 497        private void DisableHttp2Connection(Http2Connection connection)
 0498        {
 0499            if (NetEventSource.Log.IsEnabled()) connection.Trace("");
 500
 0501            _ = DisableHttp2ConnectionAsync(connection); // ignore returned task
 502
 503            async Task DisableHttp2ConnectionAsync(Http2Connection connection)
 0504            {
 0505                bool usable = await connection.WaitForAvailableStreamsAsync().ConfigureAwait(ConfigureAwaitOptions.Force
 506
 0507                if (NetEventSource.Log.IsEnabled()) connection.Trace($"{nameof(connection.WaitForAvailableStreamsAsync)}
 508
 0509                if (usable)
 0510                {
 0511                    ReturnHttp2Connection(connection, isNewConnection: false);
 0512                }
 513                else
 0514                {
 515                    // Connection has shut down.
 0516                    lock (SyncObj)
 0517                    {
 0518                        Debug.Assert(_availableHttp2Connections is null || !_availableHttp2Connections.Contains(connecti
 0519                        Debug.Assert(_associatedHttp2ConnectionCount > 0);
 520
 0521                        _associatedHttp2ConnectionCount--;
 522
 0523                        CheckForHttp2ConnectionInjection();
 0524                    }
 525
 0526                    if (NetEventSource.Log.IsEnabled()) connection.Trace("HTTP2 connection no longer usable");
 0527                    connection.Dispose();
 0528                }
 0529            };
 0530        }
 531
 532        /// <summary>
 533        /// Called when an Http2Connection from this pool is no longer usable.
 534        /// </summary>
 535        public void InvalidateHttp2Connection(Http2Connection connection)
 0536        {
 0537            if (NetEventSource.Log.IsEnabled()) connection.Trace("");
 538
 0539            bool found = false;
 0540            lock (SyncObj)
 0541            {
 0542                if (_availableHttp2Connections is not null)
 0543                {
 0544                    Debug.Assert(_associatedHttp2ConnectionCount >= _availableHttp2Connections.Count);
 545
 0546                    int index = _availableHttp2Connections.IndexOf(connection);
 0547                    if (index != -1)
 0548                    {
 0549                        found = true;
 0550                        _availableHttp2Connections.RemoveAt(index);
 0551                        _associatedHttp2ConnectionCount--;
 0552                    }
 0553                }
 554
 0555                CheckForHttp2ConnectionInjection();
 0556            }
 557
 558            // If we found the connection in the available list, then dispose it now.
 559            // Otherwise, when we try to put it back in the pool, we will see it is shut down and dispose it (and adjust
 0560            if (found)
 0561            {
 0562                connection.Dispose();
 0563            }
 0564        }
 565
 566        public void HeartBeat()
 0567        {
 568            Http2Connection[]? localHttp2Connections;
 0569            lock (SyncObj)
 0570            {
 0571                localHttp2Connections = _availableHttp2Connections?.ToArray();
 0572            }
 573
 574            // Avoid calling HeartBeat under the lock, as it may call back into HttpConnectionPool.InvalidateHttp2Connec
 0575            if (localHttp2Connections is not null)
 0576            {
 0577                foreach (Http2Connection http2Connection in localHttp2Connections)
 0578                {
 0579                    http2Connection.HeartBeat();
 0580                }
 0581            }
 0582        }
 583
 584        private static int ScavengeHttp2ConnectionList(List<Http2Connection> list, ref List<HttpConnectionBase>? toDispo
 0585        {
 0586            int freeIndex = 0;
 0587            while (freeIndex < list.Count && list[freeIndex].IsUsable(nowTicks, pooledConnectionLifetime, pooledConnecti
 0588            {
 0589                freeIndex++;
 0590            }
 591
 592            // If freeIndex == list.Count, nothing needs to be removed.
 593            // But if it's < list.Count, at least one connection needs to be purged.
 0594            int removed = 0;
 0595            if (freeIndex < list.Count)
 0596            {
 597                // We know the connection at freeIndex is unusable, so dispose of it.
 0598                toDispose ??= new List<HttpConnectionBase>();
 0599                toDispose.Add(list[freeIndex]);
 600
 601                // Find the first item after the one to be removed that should be kept.
 0602                int current = freeIndex + 1;
 0603                while (current < list.Count)
 0604                {
 605                    // Look for the first item to be kept.  Along the way, any
 606                    // that shouldn't be kept are disposed of.
 0607                    while (current < list.Count && !list[current].IsUsable(nowTicks, pooledConnectionLifetime, pooledCon
 0608                    {
 0609                        toDispose.Add(list[current]);
 0610                        current++;
 0611                    }
 612
 613                    // If we found something to keep, copy it down to the known free slot.
 0614                    if (current < list.Count)
 0615                    {
 616                        // copy item to the free slot
 0617                        list[freeIndex++] = list[current++];
 0618                    }
 619
 620                    // Keep going until there are no more good items.
 0621                }
 622
 623                // At this point, good connections have been moved below freeIndex, and garbage connections have
 624                // been added to the dispose list, so clear the end of the list past freeIndex.
 0625                removed = list.Count - freeIndex;
 0626                list.RemoveRange(freeIndex, removed);
 0627            }
 628
 0629            return removed;
 0630        }
 631    }
 632}

D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\ConnectionPool\HttpConnectionPool.Http3.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Collections.Generic;
 5using System.Diagnostics;
 6using System.Diagnostics.CodeAnalysis;
 7using System.Net.Http.Headers;
 8using System.Net.Quic;
 9using System.Net.Security;
 10using System.Runtime.ExceptionServices;
 11using System.Runtime.Versioning;
 12using System.Threading;
 13using System.Threading.Tasks;
 14
 15namespace System.Net.Http
 16{
 17    internal sealed partial class HttpConnectionPool
 18    {
 19        /// <summary>
 20        /// If <see cref="_altSvcBlocklist"/> exceeds this size, Alt-Svc will be disabled entirely for <see cref="AltSvc
 21        /// This is to prevent a failing server from bloating the dictionary beyond a reasonable value.
 22        /// </summary>
 23        private const int MaxAltSvcIgnoreListSize = 8;
 24
 25        /// <summary>The time, in milliseconds, that an authority should remain in <see cref="_altSvcBlocklist"/>.</summ
 26        private const int AltSvcBlocklistTimeoutInMilliseconds = 10 * 60 * 1000;
 27
 28        /// <summary>List of available HTTP/3 connections stored in the pool.</summary>
 29        private List<Http3Connection>? _availableHttp3Connections;
 30        /// <summary>The number of HTTP/3 connections associated with the pool, including in use, available, and pending
 31        private int _associatedHttp3ConnectionCount;
 32        /// <summary>Indicates whether an HTTP/3 connection is in the process of being established.</summary>
 33        private bool _pendingHttp3Connection;
 34        /// <summary>Queue of requests waiting for an HTTP/3 connection.</summary>
 35        private RequestQueue<Http3Connection?> _http3RequestQueue;
 36
 37        private bool _http3Enabled;
 38        internal readonly byte[]? _http3EncodedAuthorityHostHeader;
 39
 40        /// <summary>Initially set to null, this can be set to enable HTTP/3 based on Alt-Svc.</summary>
 41        private volatile HttpAuthority? _http3Authority;
 42
 43        /// <summary>A timer to expire <see cref="_http3Authority"/> and return the pool to <see cref="_originAuthority"
 44        private Timer? _authorityExpireTimer;
 45
 46        /// <summary>If true, the <see cref="_http3Authority"/> will persist across a network change. If false, it will 
 47        private bool _persistAuthority;
 48
 49        /// <summary>
 50        /// When an Alt-Svc authority fails due to 421 Misdirected Request, it is placed in the blocklist to be ignored
 51        /// for <see cref="AltSvcBlocklistTimeoutInMilliseconds"/> milliseconds. Initialized on first use.
 52        /// </summary>
 53        private volatile Dictionary<HttpAuthority, Exception?>? _altSvcBlocklist;
 54        private CancellationTokenSource? _altSvcBlocklistTimerCancellation;
 055        private volatile bool _altSvcEnabled = true;
 56
 057        private bool EnableMultipleHttp3Connections => _poolManager.Settings.EnableMultipleHttp3Connections;
 58
 59        // Returns null if HTTP3 cannot be used.
 60        [SupportedOSPlatform("windows")]
 61        [SupportedOSPlatform("linux")]
 62        [SupportedOSPlatform("macos")]
 63        private async ValueTask<HttpResponseMessage?> TrySendUsingHttp3Async(HttpRequestMessage request, CancellationTok
 064        {
 065            Debug.Assert(GlobalHttpSettings.SocketsHttpHandler.AllowHttp3);
 66
 067            Debug.Assert(_kind == HttpConnectionKind.Https);
 068            Debug.Assert(_http3Enabled);
 69
 70            // Loop in case we get a 421 and need to send the request to a different authority.
 071            while (true)
 072            {
 073                HttpConnectionWaiter<Http3Connection?>? http3ConnectionWaiter = null;
 74                try
 075                {
 076                    if (!TryGetHttp3Authority(request, out HttpAuthority? authority, out Exception? reasonException))
 077                    {
 078                        if (reasonException is null)
 079                        {
 080                            return null;
 81                        }
 082                        ThrowGetVersionException(request, 3, reasonException);
 083                    }
 84
 085                    WaitForHttp3ConnectionActivity waitForConnectionActivity = new WaitForHttp3ConnectionActivity(Settin
 086                    if (!TryGetPooledHttp3Connection(request, out Http3Connection? connection, out http3ConnectionWaiter
 087                    {
 088                        waitForConnectionActivity.Start();
 89                        try
 090                        {
 091                            connection = await http3ConnectionWaiter.WaitWithCancellationAsync(cancellationToken).Config
 092                        }
 093                        catch (Exception ex)
 094                        {
 095                            waitForConnectionActivity.Stop(request, this, ex);
 096                            throw;
 97                        }
 098                    }
 99
 100                    // Request cannot be sent over H/3 connection, try downgrade or report failure.
 101                    // Note that if there's an H/3 suitable origin authority but is unavailable or blocked via Alt-Svc, 
 0102                    if (connection is null)
 0103                    {
 0104                        return null;
 105                    }
 106
 0107                    HttpResponseMessage response = await connection.SendAsync(request, waitForConnectionActivity, stream
 108
 109                    // If an Alt-Svc authority returns 421, it means it can't actually handle the request.
 110                    // An authority is supposed to be able to handle ALL requests to the origin, so this is a server bug
 111                    // In this case, we blocklist the authority and retry the request at the origin.
 0112                    if (response.StatusCode == HttpStatusCode.MisdirectedRequest && connection.Authority != _originAutho
 0113                    {
 0114                        response.Dispose();
 0115                        BlocklistAuthority(connection.Authority);
 0116                        continue;
 117                    }
 118
 0119                    return response;
 120                }
 121                finally
 0122                {
 0123                    http3ConnectionWaiter?.SetTimeoutToPendingConnectionAttempt(this, cancellationToken.IsCancellationRe
 0124                }
 125            }
 0126        }
 127
 128        [SupportedOSPlatform("windows")]
 129        [SupportedOSPlatform("linux")]
 130        [SupportedOSPlatform("macos")]
 131        private bool TryGetPooledHttp3Connection(HttpRequestMessage request, [NotNullWhen(true)] out Http3Connection? co
 0132        {
 0133            Debug.Assert(GlobalHttpSettings.SocketsHttpHandler.AllowHttp3);
 134
 135            // Look for a usable connection.
 0136            while (true)
 0137            {
 0138                lock (SyncObj)
 0139                {
 0140                    int availableConnectionCount = _availableHttp3Connections?.Count ?? 0;
 0141                    if (availableConnectionCount > 0)
 0142                    {
 143                        // We have a connection that we can attempt to use.
 144                        // Validate it below outside the lock, to avoid doing expensive operations while holding the loc
 0145                        connection = _availableHttp3Connections![availableConnectionCount - 1];
 0146                    }
 147                    else
 0148                    {
 149                        // No available connections. Add to the request queue.
 0150                        waiter = _http3RequestQueue.EnqueueRequest(request);
 151
 0152                        CheckForHttp3ConnectionInjection();
 153
 154                        // There were no available connections. This request has been added to the request queue.
 0155                        if (NetEventSource.Log.IsEnabled()) Trace($"No available HTTP/3 connections; request queued.");
 0156                        connection = null;
 0157                        streamAvailable = false;
 0158                        return false;
 159                    }
 0160                }
 161
 0162                if (CheckExpirationOnGet(connection))
 0163                {
 0164                    if (NetEventSource.Log.IsEnabled()) connection.Trace("Found expired HTTP/3 connection in pool.");
 165
 0166                    InvalidateHttp3Connection(connection);
 0167                    continue;
 168                }
 169
 0170                streamAvailable = connection.TryReserveStream();
 171
 172                // Disable and remove the connection from the pool only if we can open another.
 173                // If we have only single connection, use the underlying QuicConnection mechanism to wait for available 
 0174                if (!streamAvailable && EnableMultipleHttp3Connections)
 0175                {
 0176                    if (NetEventSource.Log.IsEnabled()) connection.Trace("Found HTTP/3 connection in pool without availa
 177
 0178                    bool found = false;
 0179                    lock (SyncObj)
 0180                    {
 0181                        int index = _availableHttp3Connections.IndexOf(connection);
 0182                        if (index != -1)
 0183                        {
 0184                            found = true;
 0185                            _availableHttp3Connections.RemoveAt(index);
 0186                        }
 0187                    }
 188
 189                    // If we didn't find the connection, then someone beat us to removing it (or it shut down)
 0190                    if (found)
 0191                    {
 0192                        DisableHttp3Connection(connection);
 0193                    }
 0194                    continue;
 195                }
 196
 0197                if (NetEventSource.Log.IsEnabled()) connection.Trace("Found usable HTTP/3 connection in pool.");
 0198                waiter = null;
 0199                return true;
 200            }
 0201        }
 202
 203        [SupportedOSPlatform("windows")]
 204        [SupportedOSPlatform("linux")]
 205        [SupportedOSPlatform("macos")]
 206        private void CheckForHttp3ConnectionInjection()
 0207        {
 0208            Debug.Assert(GlobalHttpSettings.SocketsHttpHandler.AllowHttp3);
 209
 0210            Debug.Assert(HasSyncObjLock);
 211
 0212            _http3RequestQueue.PruneCompletedRequestsFromHeadOfQueue(this);
 213
 214            // Determine if we can and should add a new connection to the pool.
 0215            int availableHttp3ConnectionCount = _availableHttp3Connections?.Count ?? 0;
 0216            bool willInject = availableHttp3ConnectionCount == 0 &&                         // No available connections
 0217                !_pendingHttp3Connection &&                                                 // Only allow one pending HT
 0218                _http3RequestQueue.Count > 0 &&                                             // There are requests left o
 0219                (_associatedHttp3ConnectionCount == 0 || EnableMultipleHttp3Connections) && // We allow multiple connect
 0220                _http3RequestQueue.RequestsWithoutAConnectionAttempt > 0;                   // There are requests we hav
 221
 0222            if (NetEventSource.Log.IsEnabled())
 0223            {
 0224                Trace($"Available HTTP/3.0 connections: {availableHttp3ConnectionCount}, " +
 0225                    $"Pending HTTP/3.0 connection: {_pendingHttp3Connection}, " +
 0226                    $"Requests in the queue: {_http3RequestQueue.Count}, " +
 0227                    $"Requests without a connection attempt: {_http3RequestQueue.RequestsWithoutAConnectionAttempt}, " +
 0228                    $"Total associated HTTP/3.0 connections: {_associatedHttp3ConnectionCount}, " +
 0229                    $"Will inject connection: {willInject}.");
 0230            }
 231
 0232            if (willInject)
 0233            {
 0234                _associatedHttp3ConnectionCount++;
 0235                _pendingHttp3Connection = true;
 236
 0237                RequestQueue<Http3Connection?>.QueueItem queueItem = _http3RequestQueue.PeekNextRequestForConnectionAtte
 0238                _ = InjectNewHttp3ConnectionAsync(queueItem); // ignore returned task
 0239            }
 0240        }
 241
 242        [SupportedOSPlatform("windows")]
 243        [SupportedOSPlatform("linux")]
 244        [SupportedOSPlatform("macos")]
 245        private async Task InjectNewHttp3ConnectionAsync(RequestQueue<Http3Connection?>.QueueItem queueItem)
 0246        {
 0247            Debug.Assert(GlobalHttpSettings.SocketsHttpHandler.AllowHttp3);
 248
 0249            if (NetEventSource.Log.IsEnabled()) Trace("Creating new HTTP/3 connection for pool.");
 250
 251            // Queue the remainder of the work so that this method completes quickly
 252            // and escapes locks held by the caller.
 0253            await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
 254
 0255            Http3Connection? connection = null;
 0256            Exception? connectionException = null;
 0257            HttpAuthority? authority = null;
 0258            HttpConnectionWaiter<Http3Connection?> waiter = queueItem.Waiter;
 259
 0260            CancellationTokenSource cts = GetConnectTimeoutCancellationTokenSource(waiter);
 0261            Activity? connectionSetupActivity = null;
 262            try
 0263            {
 0264                if (TryGetHttp3Authority(queueItem.Request, out authority, out Exception? reasonException))
 0265                {
 0266                    connectionSetupActivity = ConnectionSetupDistributedTracing.StartConnectionSetupActivity(isSecure: t
 267                    // If the authority was sent as an option through alt-svc then include alt-used header.
 0268                    connection = new Http3Connection(this, authority, includeAltUsedHeader: _http3Authority == authority
 0269                    QuicConnection quicConnection = await ConnectHelper.ConnectQuicAsync(queueItem.Request, new DnsEndPo
 0270                    if (quicConnection.NegotiatedApplicationProtocol != SslApplicationProtocol.Http3)
 0271                    {
 0272                        await quicConnection.DisposeAsync().ConfigureAwait(false);
 0273                        throw new HttpRequestException(HttpRequestError.ConnectionError, "QUIC connected but no HTTP/3 i
 274                    }
 0275                    if (connectionSetupActivity is not null) ConnectionSetupDistributedTracing.StopConnectionSetupActivi
 0276                    connection.InitQuicConnection(quicConnection, connectionSetupActivity);
 0277                }
 0278                else if (reasonException is not null)
 0279                {
 0280                    ThrowGetVersionException(queueItem.Request, 3, reasonException);
 0281                }
 0282            }
 0283            catch (Exception e)
 0284            {
 0285                connectionException = e is OperationCanceledException oce && oce.CancellationToken == cts.Token && !wait
 0286                    CreateConnectTimeoutException(oce) :
 0287                    e;
 288
 289                // On success path connectionSetupActivity is stopped before calling InitQuicConnection().
 290                // This assertion makes sure that InitQuicConnection() does not throw unexpectedly.
 0291                Debug.Assert(connectionSetupActivity?.IsStopped is not true);
 0292                if (connectionSetupActivity is not null) ConnectionSetupDistributedTracing.StopConnectionSetupActivity(c
 293
 294                // If the connection hasn't been initialized with QuicConnection, get rid of it.
 0295                connection?.Dispose();
 0296                connection = null;
 0297            }
 298            finally
 0299            {
 0300                lock (waiter)
 0301                {
 0302                    waiter.ConnectionCancellationTokenSource = null;
 0303                    cts.Dispose();
 0304                }
 0305            }
 306
 0307            if (connection is not null)
 0308            {
 309                // Add the new connection to the pool.
 0310                ReturnHttp3Connection(connection, isNewConnection: true, waiter);
 0311            }
 312            else
 0313            {
 314                // Block list authority only if the connection attempt was not cancelled.
 0315                if (connectionException is not null && connectionException is not OperationCanceledException && authorit
 0316                {
 317                    // Disables HTTP/3 until server announces it can handle it via Alt-Svc.
 0318                    BlocklistAuthority(authority, connectionException);
 0319                }
 320
 0321                HandleHttp3ConnectionFailure(waiter, connectionException);
 0322            }
 0323        }
 324
 325        [SupportedOSPlatform("windows")]
 326        [SupportedOSPlatform("linux")]
 327        [SupportedOSPlatform("macos")]
 328        private void HandleHttp3ConnectionFailure(HttpConnectionWaiter<Http3Connection?> requestWaiter, Exception? e)
 0329        {
 0330            Debug.Assert(GlobalHttpSettings.SocketsHttpHandler.AllowHttp3);
 331
 0332            if (NetEventSource.Log.IsEnabled()) Trace($"HTTP3 connection failed: {e}");
 333
 334            // We don't care if this fails; that means the request was previously canceled or handled by a different con
 0335            if (e is null)
 0336            {
 0337                requestWaiter.TrySetResult(null);
 0338            }
 339            else
 0340            {
 0341                requestWaiter.TrySetException(e);
 0342            }
 343
 0344            lock (SyncObj)
 0345            {
 0346                Debug.Assert(_associatedHttp3ConnectionCount > 0);
 0347                Debug.Assert(_pendingHttp3Connection);
 348
 0349                _associatedHttp3ConnectionCount--;
 0350                _pendingHttp3Connection = false;
 351
 0352                CheckForHttp3ConnectionInjection();
 0353            }
 0354        }
 355
 356        [SupportedOSPlatform("windows")]
 357        [SupportedOSPlatform("linux")]
 358        [SupportedOSPlatform("macos")]
 359        private void ReturnHttp3Connection(Http3Connection connection, bool isNewConnection, HttpConnectionWaiter<Http3C
 0360        {
 0361            Debug.Assert(GlobalHttpSettings.SocketsHttpHandler.AllowHttp3);
 362
 0363            if (NetEventSource.Log.IsEnabled()) connection.Trace($"{nameof(isNewConnection)}={isNewConnection}");
 364
 0365            Debug.Assert(!HasSyncObjLock);
 0366            Debug.Assert(isNewConnection || initialRequestWaiter is null, "Shouldn't have a request unless the connectio
 367
 0368            if (!isNewConnection && CheckExpirationOnReturn(connection))
 0369            {
 0370                lock (SyncObj)
 0371                {
 0372                    Debug.Assert(_availableHttp3Connections is null || !_availableHttp3Connections.Contains(connection))
 0373                    Debug.Assert(_associatedHttp3ConnectionCount > (_availableHttp3Connections?.Count ?? 0));
 0374                    _associatedHttp3ConnectionCount--;
 0375                }
 376
 0377                if (NetEventSource.Log.IsEnabled()) connection.Trace("Disposing HTTP3 connection return to pool. Connect
 0378                connection.Dispose();
 0379                return;
 380            }
 381
 0382            while (connection.TryReserveStream() || !EnableMultipleHttp3Connections)
 0383            {
 384                // Loop in case we get a request that has already been canceled or handled by a different connection.
 0385                while (true)
 0386                {
 0387                    HttpConnectionWaiter<Http3Connection?>? waiter = null;
 0388                    bool added = false;
 0389                    lock (SyncObj)
 0390                    {
 0391                        Debug.Assert(_availableHttp3Connections is null || !_availableHttp3Connections.Contains(connecti
 0392                        Debug.Assert(_associatedHttp3ConnectionCount > (_availableHttp3Connections?.Count ?? 0),
 0393                            $"Expected _associatedHttp3ConnectionCount={_associatedHttp3ConnectionCount} > _availableHtt
 394
 0395                        if (isNewConnection)
 0396                        {
 0397                            Debug.Assert(_pendingHttp3Connection);
 0398                            _pendingHttp3Connection = false;
 0399                            isNewConnection = false;
 0400                        }
 401
 0402                        if (initialRequestWaiter is not null)
 0403                        {
 404                            // Try to handle the request that we initiated the connection for first
 0405                            waiter = initialRequestWaiter;
 0406                            initialRequestWaiter = null;
 407
 408                            // If this method found a request to service, that request must be removed from the queue if
 409                            // Normally, TryDequeueWaiter would handle the removal. TryDequeueSpecificWaiter matches thi
 410                            // We don't care if this fails; that means the request was previously canceled, handled by a
 0411                            _http3RequestQueue.TryDequeueSpecificWaiter(waiter);
 0412                        }
 0413                        else if (_http3RequestQueue.TryDequeueWaiter(this, out waiter))
 0414                        {
 0415                            Debug.Assert((_availableHttp3Connections?.Count ?? 0) == 0, $"With {(_availableHttp3Connecti
 0416                        }
 0417                        else if (_disposed)
 0418                        {
 419                            // The pool has been disposed. We will dispose this connection below outside the lock.
 420                            // We do this check after processing the request queue so that any queued requests will be h
 0421                            _associatedHttp3ConnectionCount--;
 0422                        }
 423                        else
 0424                        {
 425                            // Add connection to the pool.
 0426                            added = true;
 0427                            _availableHttp3Connections ??= new List<Http3Connection>();
 0428                            _availableHttp3Connections.Add(connection);
 0429                        }
 0430                    }
 431
 0432                    if (waiter is not null)
 0433                    {
 0434                        Debug.Assert(!added);
 435
 0436                        if (waiter.TrySignal(connection))
 0437                        {
 0438                            break;
 439                        }
 440
 441                        // Loop and process the queue again
 0442                    }
 443                    else
 0444                    {
 445                        // TryReserveStream() always decrements the available stream counter when EnableMultipleHttp3Con
 0446                        connection.ReleaseStream();
 447
 0448                        if (added)
 0449                        {
 0450                            if (NetEventSource.Log.IsEnabled()) connection.Trace("Put HTTP3 connection in pool.");
 0451                            return;
 452                        }
 453                        else
 0454                        {
 0455                            Debug.Assert(_disposed);
 0456                            if (NetEventSource.Log.IsEnabled()) connection.Trace("Disposing HTTP3 connection returned to
 0457                            connection.Dispose();
 0458                            return;
 459                        }
 460                    }
 0461                }
 0462            }
 463
 464            // Since we only inject one connection at a time, we may want to inject another now.
 0465            lock (SyncObj)
 0466            {
 0467                CheckForHttp3ConnectionInjection();
 0468            }
 469
 470            // We need to wait until the connection is usable again.
 0471            DisableHttp3Connection(connection);
 0472        }
 473
 474        /// <summary>
 475        /// Disable usage of the specified connection because it cannot handle any more streams at the moment.
 476        /// We will register to be notified when it can handle more streams (or becomes permanently unusable).
 477        /// </summary>
 478        [SupportedOSPlatform("windows")]
 479        [SupportedOSPlatform("linux")]
 480        [SupportedOSPlatform("macos")]
 481        private void DisableHttp3Connection(Http3Connection connection)
 0482        {
 0483            Debug.Assert(GlobalHttpSettings.SocketsHttpHandler.AllowHttp3);
 484
 0485            if (NetEventSource.Log.IsEnabled()) connection.Trace("");
 486
 0487            _ = DisableHttp3ConnectionAsync(connection); // ignore returned task
 488
 489            async Task DisableHttp3ConnectionAsync(Http3Connection connection)
 0490            {
 0491                bool usable = await connection.WaitForAvailableStreamsAsync().ConfigureAwait(ConfigureAwaitOptions.Force
 492
 0493                if (NetEventSource.Log.IsEnabled()) connection.Trace($"{nameof(connection.WaitForAvailableStreamsAsync)}
 494
 0495                if (usable)
 0496                {
 0497                    ReturnHttp3Connection(connection, isNewConnection: false);
 0498                }
 499                else
 0500                {
 501                    // Connection has shut down.
 0502                    lock (SyncObj)
 0503                    {
 0504                        Debug.Assert(_availableHttp3Connections is null || !_availableHttp3Connections.Contains(connecti
 0505                        Debug.Assert(_associatedHttp3ConnectionCount > 0);
 506
 0507                        _associatedHttp3ConnectionCount--;
 508
 0509                        CheckForHttp3ConnectionInjection();
 0510                    }
 511
 0512                    if (NetEventSource.Log.IsEnabled()) connection.Trace("HTTP3 connection no longer usable");
 0513                    connection.Dispose();
 0514                }
 0515            };
 0516        }
 517
 518        /// <summary>
 519        /// Called when an Http3Connection from this pool is no longer usable.
 520        /// </summary>
 521        [SupportedOSPlatform("windows")]
 522        [SupportedOSPlatform("linux")]
 523        [SupportedOSPlatform("macos")]
 524        public void InvalidateHttp3Connection(Http3Connection connection, bool dispose = true)
 0525        {
 0526            Debug.Assert(GlobalHttpSettings.SocketsHttpHandler.AllowHttp3);
 527
 0528            if (NetEventSource.Log.IsEnabled()) connection.Trace("");
 529
 0530            bool found = false;
 0531            lock (SyncObj)
 0532            {
 0533                if (_availableHttp3Connections is not null)
 0534                {
 0535                    Debug.Assert(_associatedHttp3ConnectionCount >= _availableHttp3Connections.Count);
 536
 0537                    int index = _availableHttp3Connections.IndexOf(connection);
 0538                    if (index != -1)
 0539                    {
 0540                        found = true;
 0541                        _availableHttp3Connections.RemoveAt(index);
 0542                        _associatedHttp3ConnectionCount--;
 0543                    }
 0544                }
 545
 0546                CheckForHttp3ConnectionInjection();
 0547            }
 548
 549            // If we found the connection in the available list, then dispose it now.
 550            // Otherwise, when we try to put it back in the pool, we will see it is shut down and dispose it (and adjust
 0551            if (found && dispose)
 0552            {
 0553                connection.Dispose();
 0554            }
 0555        }
 556
 557        [SupportedOSPlatform("windows")]
 558        [SupportedOSPlatform("linux")]
 559        [SupportedOSPlatform("macos")]
 560        private static int ScavengeHttp3ConnectionList(List<Http3Connection> list, ref List<HttpConnectionBase>? toDispo
 0561        {
 0562            Debug.Assert(GlobalHttpSettings.SocketsHttpHandler.AllowHttp3);
 563
 0564            int freeIndex = 0;
 0565            while (freeIndex < list.Count && list[freeIndex].IsUsable(nowTicks, pooledConnectionLifetime, pooledConnecti
 0566            {
 0567                freeIndex++;
 0568            }
 569
 570            // If freeIndex == list.Count, nothing needs to be removed.
 571            // But if it's < list.Count, at least one connection needs to be purged.
 0572            int removed = 0;
 0573            if (freeIndex < list.Count)
 0574            {
 575                // We know the connection at freeIndex is unusable, so dispose of it.
 0576                toDispose ??= new List<HttpConnectionBase>();
 0577                toDispose.Add(list[freeIndex]);
 578
 579                // Find the first item after the one to be removed that should be kept.
 0580                int current = freeIndex + 1;
 0581                while (current < list.Count)
 0582                {
 583                    // Look for the first item to be kept.  Along the way, any
 584                    // that shouldn't be kept are disposed of.
 0585                    while (current < list.Count && !list[current].IsUsable(nowTicks, pooledConnectionLifetime, pooledCon
 0586                    {
 0587                        toDispose.Add(list[current]);
 0588                        current++;
 0589                    }
 590
 591                    // If we found something to keep, copy it down to the known free slot.
 0592                    if (current < list.Count)
 0593                    {
 594                        // copy item to the free slot
 0595                        list[freeIndex++] = list[current++];
 0596                    }
 597
 598                    // Keep going until there are no more good items.
 0599                }
 600
 601                // At this point, good connections have been moved below freeIndex, and garbage connections have
 602                // been added to the dispose list, so clear the end of the list past freeIndex.
 0603                removed = list.Count - freeIndex;
 0604                list.RemoveRange(freeIndex, removed);
 0605            }
 606
 0607            return removed;
 0608        }
 609
 610        private bool TryGetHttp3Authority(HttpRequestMessage request, [NotNullWhen(true)] out HttpAuthority? authority, 
 0611        {
 0612            authority = _http3Authority;
 613
 614            // If H3 is explicitly requested, assume pre-negotiated H3.
 0615            if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower)
 0616            {
 0617                authority ??= _originAuthority;
 0618            }
 619
 0620            if (authority is null)
 0621            {
 0622                reasonException = null;
 0623                return false;
 624            }
 625
 0626            if (IsAltSvcBlocked(authority, out reasonException))
 0627            {
 0628                return false;
 629            }
 630
 0631            return true;
 0632        }
 633
 634
 635        /// <summary>Check for the Alt-Svc header, to upgrade to HTTP/3.</summary>
 636        private void ProcessAltSvc(HttpResponseMessage response)
 0637        {
 0638            if (_altSvcEnabled && response.Headers.TryGetValues(KnownHeaders.AltSvc.Descriptor, out IEnumerable<string>?
 0639            {
 0640                HandleAltSvc(altSvcHeaderValues, response.Headers.Age);
 0641            }
 0642        }
 643
 644        /// <summary>
 645        /// Inspects a collection of Alt-Svc headers to find the first eligible upgrade path.
 646        /// </summary>
 647        /// <remarks>TODO: common case will likely be a single value. Optimize for that.</remarks>
 648        internal void HandleAltSvc(IEnumerable<string> altSvcHeaderValues, TimeSpan? responseAge)
 0649        {
 0650            HttpAuthority? nextAuthority = null;
 0651            TimeSpan nextAuthorityMaxAge = default;
 0652            bool nextAuthorityPersist = false;
 653
 0654            foreach (string altSvcHeaderValue in altSvcHeaderValues)
 0655            {
 0656                int parseIdx = 0;
 657
 0658                if (AltSvcHeaderParser.Parser.TryParseValue(altSvcHeaderValue, null, ref parseIdx, out object? parsedVal
 0659                {
 0660                    Debug.Assert(parsedValue is not null);
 661
 0662                    var value = (AltSvcHeaderValue)parsedValue;
 663
 664                    // 'clear' should be the only value present.
 0665                    if (ReferenceEquals(AltSvcHeaderValue.Clear, value))
 0666                    {
 0667                        lock (SyncObj)
 0668                        {
 669                            // Clear invalidates all Alt-Svc including the current response ones.
 670                            // https://httpwg.org/specs/rfc7838.html#alt-svc
 0671                            ExpireAltSvcAuthority();
 0672                            Debug.Assert(_authorityExpireTimer != null || _disposed);
 0673                            _authorityExpireTimer?.Change(Timeout.Infinite, Timeout.Infinite);
 0674                            return;
 675                        }
 676                    }
 677
 678                    // Do not process the Alt-Svc header if we've already found a valid h3 authority before, but continu
 0679                    if (nextAuthority is not null || value.AlpnProtocolName != "h3")
 0680                    {
 0681                        continue;
 682                    }
 683
 0684                    var authority = new HttpAuthority(value.Host ?? _originAuthority.IdnHost, value.Port);
 0685                    if (IsAltSvcBlocked(authority, out _))
 0686                    {
 687                        // Skip authorities in our blocklist.
 0688                        continue;
 689                    }
 690
 0691                    TimeSpan authorityMaxAge = value.MaxAge;
 692
 0693                    if (responseAge != null)
 0694                    {
 0695                        authorityMaxAge -= responseAge.GetValueOrDefault();
 0696                    }
 697
 698                    // It's already out of date, skip it.
 0699                    if (authorityMaxAge <= TimeSpan.Zero)
 0700                    {
 0701                        continue;
 702                    }
 703
 704                    // We found an h3 authority that is not blocked and has not aged out.
 0705                    nextAuthority = authority;
 0706                    nextAuthorityMaxAge = authorityMaxAge;
 0707                    nextAuthorityPersist = value.Persist;
 0708                }
 0709            }
 710
 711            // There's a race here in checking _http3Authority outside of the lock,
 712            // but there's really no bad behavior if _http3Authority changes in the mean time.
 0713            if (nextAuthority != null && !nextAuthority.Equals(_http3Authority))
 0714            {
 715                // Clamp the max age to 30 days... this is arbitrary but prevents passing a too-large TimeSpan to the Ti
 0716                if (nextAuthorityMaxAge.Ticks > (30 * TimeSpan.TicksPerDay))
 0717                {
 0718                    nextAuthorityMaxAge = TimeSpan.FromTicks(30 * TimeSpan.TicksPerDay);
 0719                }
 720
 0721                lock (SyncObj)
 0722                {
 0723                    if (_disposed)
 0724                    {
 725                        // avoid creating or touching _authorityExpireTimer after disposal
 0726                        return;
 727                    }
 728
 0729                    if (_authorityExpireTimer == null)
 0730                    {
 0731                        var thisRef = new WeakReference<HttpConnectionPool>(this);
 732
 0733                        using (ExecutionContext.SuppressFlow())
 0734                        {
 0735                            _authorityExpireTimer = new Timer(static o =>
 0736                            {
 0737                                var wr = (WeakReference<HttpConnectionPool>)o!;
 0738                                if (wr.TryGetTarget(out HttpConnectionPool? @this))
 0739                                {
 0740                                    lock (@this.SyncObj)
 0741                                    {
 0742                                        @this.ExpireAltSvcAuthority();
 0743                                    }
 0744                                }
 0745                            }, thisRef, nextAuthorityMaxAge, Timeout.InfiniteTimeSpan);
 0746                        }
 0747                    }
 748                    else
 0749                    {
 0750                        _authorityExpireTimer.Change(nextAuthorityMaxAge, Timeout.InfiniteTimeSpan);
 0751                    }
 752
 0753                    _http3Authority = nextAuthority;
 0754                    _persistAuthority = nextAuthorityPersist;
 0755                }
 756
 0757                if (!nextAuthorityPersist)
 0758                {
 759#if !ILLUMOS && !SOLARIS && !HAIKU
 0760                    _poolManager.StartMonitoringNetworkChanges();
 761#endif
 0762                }
 0763            }
 0764        }
 765
 766        /// <summary>
 767        /// Expires the current Alt-Svc authority, resetting the connection back to origin.
 768        /// </summary>
 769        private void ExpireAltSvcAuthority()
 0770        {
 0771            Debug.Assert(HasSyncObjLock);
 772
 773            // If we ever support prenegotiated HTTP/3, this should be set to origin, not nulled out.
 0774            _http3Authority = null;
 0775        }
 776
 777        /// <summary>
 778        /// Checks whether the given <paramref name="authority"/> is on the currext Alt-Svc blocklist.
 779        /// If it is, then it places the cause in the <paramref name="reasonException"/>
 780        /// </summary>
 781        /// <seealso cref="BlocklistAuthority" />
 782        private bool IsAltSvcBlocked(HttpAuthority authority, out Exception? reasonException)
 0783        {
 0784            if (_altSvcBlocklist != null)
 0785            {
 0786                lock (_altSvcBlocklist)
 0787                {
 0788                    return _altSvcBlocklist.TryGetValue(authority, out reasonException);
 789                }
 790            }
 0791            reasonException = null;
 0792            return false;
 0793        }
 794
 795        /// <summary>
 796        /// Blocklists an authority and resets the current authority back to origin.
 797        /// If the number of blocklisted authorities exceeds <see cref="MaxAltSvcIgnoreListSize"/>,
 798        /// Alt-Svc will be disabled entirely for a period of time.
 799        /// </summary>
 800        /// <remarks>
 801        /// This is called when we get a "421 Misdirected Request" from an alternate authority.
 802        /// A future strategy would be to retry the individual request on an older protocol, we'd want to have
 803        /// some logic to blocklist after some number of failures to avoid doubling our request latency.
 804        ///
 805        /// For now, the spec states alternate authorities should be able to handle ALL requests, so this
 806        /// is treated as an exceptional error by immediately blocklisting the authority.
 807        /// </remarks>
 808        internal void BlocklistAuthority(HttpAuthority badAuthority, Exception? exception = null)
 0809        {
 0810            Debug.Assert(badAuthority != null);
 811
 0812            Dictionary<HttpAuthority, Exception?>? altSvcBlocklist = _altSvcBlocklist;
 813
 0814            if (altSvcBlocklist == null)
 0815            {
 0816                lock (SyncObj)
 0817                {
 0818                    if (_disposed)
 0819                    {
 820                        // avoid creating _altSvcBlocklistTimerCancellation after disposal
 0821                        return;
 822                    }
 823
 0824                    altSvcBlocklist = _altSvcBlocklist;
 0825                    if (altSvcBlocklist == null)
 0826                    {
 0827                        altSvcBlocklist = new Dictionary<HttpAuthority, Exception?>();
 0828                        _altSvcBlocklistTimerCancellation = new CancellationTokenSource();
 0829                        _altSvcBlocklist = altSvcBlocklist;
 0830                    }
 0831                }
 0832            }
 833
 0834            bool added, disabled = false;
 835
 0836            lock (altSvcBlocklist)
 0837            {
 0838                added = altSvcBlocklist.TryAdd(badAuthority, exception);
 839
 0840                if (added && altSvcBlocklist.Count >= MaxAltSvcIgnoreListSize && _altSvcEnabled)
 0841                {
 0842                    _altSvcEnabled = false;
 0843                    disabled = true;
 0844                }
 0845            }
 846
 847            CancellationToken altSvcBlocklistTimerCt;
 848
 0849            lock (SyncObj)
 0850            {
 0851                if (_disposed)
 0852                {
 853                    // avoid touching _authorityExpireTimer and _altSvcBlocklistTimerCancellation after disposal
 0854                    return;
 855                }
 856
 0857                if (_http3Authority == badAuthority)
 0858                {
 0859                    ExpireAltSvcAuthority();
 0860                    Debug.Assert(_authorityExpireTimer != null);
 0861                    _authorityExpireTimer.Change(Timeout.Infinite, Timeout.Infinite);
 0862                }
 863
 0864                Debug.Assert(_altSvcBlocklistTimerCancellation != null);
 0865                altSvcBlocklistTimerCt = _altSvcBlocklistTimerCancellation.Token;
 0866            }
 867
 0868            if (added)
 0869            {
 0870                _ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds, altSvcBlocklistTimerCt)
 0871                    .ContinueWith(t =>
 0872                    {
 0873                        lock (altSvcBlocklist)
 0874                        {
 0875                            altSvcBlocklist.Remove(badAuthority);
 0876                        }
 0877                    }, altSvcBlocklistTimerCt, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
 0878            }
 879
 0880            if (disabled)
 0881            {
 0882                _ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds, altSvcBlocklistTimerCt)
 0883                    .ContinueWith(t =>
 0884                    {
 0885                        _altSvcEnabled = true;
 0886                    }, altSvcBlocklistTimerCt, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
 0887            }
 0888        }
 889
 890        public void OnNetworkChanged()
 0891        {
 0892            lock (SyncObj)
 0893            {
 0894                if (_http3Authority != null && !_persistAuthority)
 0895                {
 0896                    ExpireAltSvcAuthority();
 0897                    Debug.Assert(_authorityExpireTimer != null || _disposed);
 0898                    _authorityExpireTimer?.Change(Timeout.Infinite, Timeout.Infinite);
 0899                }
 0900            }
 0901        }
 902    }
 903}

Methods/Properties

.cctor()
.ctor(System.Net.Http.HttpConnectionPoolManager,System.Net.Http.HttpConnectionKind,System.String,System.Int32,System.String,System.Uri,System.String)
ConstructSslOptions(System.Net.Http.HttpConnectionPoolManager,System.String)
TelemetryServerAddress()
OriginAuthority()
Settings()
Kind()
IsSecure()
ProxyUri()
ProxyCredentials()
PreAuthCredentials()
IsDefaultPort()
DoProxyAuth()
SyncObj()
HasSyncObjLock()
SendAsync(System.Net.Http.HttpRequestMessage,System.Boolean,System.Boolean,System.Threading.CancellationToken)
SendWithProxyAuthAsync(System.Net.Http.HttpRequestMessage,System.Boolean,System.Boolean,System.Threading.CancellationToken)
SendWithNtConnectionAuthAsync(System.Net.Http.HttpConnection,System.Net.Http.HttpRequestMessage,System.Boolean,System.Boolean,System.Threading.CancellationToken)
SendWithNtProxyAuthAsync(System.Net.Http.HttpConnection,System.Net.Http.HttpRequestMessage,System.Boolean,System.Threading.CancellationToken)
SendWithVersionDetectionAndRetryAsync()
ConnectAsync()
GetRemoteEndPoint(System.IO.Stream)
ConnectToTcpHostAsync()
GetSslOptionsForRequest(System.Net.Http.HttpRequestMessage)
ApplyPlaintextFilterAsync()
EstablishProxyTunnelAsync()
EstablishSocksTunnel()
GetConnectTimeoutCancellationTokenSource(System.Net.Http.HttpConnectionWaiter`1<T>)
CreateConnectTimeoutException(System.OperationCanceledException)
ThrowGetVersionException(System.Net.Http.HttpRequestMessage,System.Int32,System.Exception)
CheckExpirationOnGet(System.Net.Http.HttpConnectionBase)
CheckExpirationOnReturn(System.Net.Http.HttpConnectionBase)
Dispose()
CleanCacheAndDisposeIfUnused()
ToString()
Trace(System.String,System.String)
.ctor(System.Net.Http.HttpConnectionPoolManager,System.Net.Http.HttpConnectionKind,System.String,System.Int32,System.String,System.Uri,System.String)
HostHeaderLineBytes()
TryGetPooledHttp11Connection(System.Net.Http.HttpRequestMessage,System.Boolean,System.Net.Http.HttpConnection&,System.Net.Http.HttpConnectionWaiter`1<System.Net.Http.HttpConnection>&)
ProcessHttp11RequestQueue(System.Net.Http.HttpConnection)
CheckForHttp11ConnectionInjection()
InjectNewHttp11ConnectionAsync()
CreateHttp11ConnectionAsync()
ConstructHttp11ConnectionAsync()
HandleHttp11ConnectionFailure(System.Net.Http.HttpConnectionWaiter`1<System.Net.Http.HttpConnection>,System.Exception)
RecycleHttp11Connection(System.Net.Http.HttpConnection)
AddNewHttp11Connection(System.Net.Http.HttpConnection,System.Net.Http.HttpConnectionWaiter`1<System.Net.Http.HttpConnection>)
ReturnHttp11Connection(System.Net.Http.HttpConnection)
InvalidateHttp11Connection(System.Net.Http.HttpConnection,System.Boolean)
ScavengeHttp11ConnectionStack(System.Net.Http.HttpConnectionPool,System.Collections.Concurrent.ConcurrentStack`1<System.Net.Http.HttpConnection>,System.Collections.Generic.List`1<System.Net.Http.HttpConnectionBase>&,System.Int64,System.TimeSpan,System.TimeSpan)
Http2AltSvcOriginUri()
EnableMultipleHttp2Connections()
TryGetPooledHttp2Connection(System.Net.Http.HttpRequestMessage,System.Net.Http.Http2Connection&,System.Net.Http.HttpConnectionWaiter`1<System.Net.Http.Http2Connection>&)
CheckForHttp2ConnectionInjection()
InjectNewHttp2ConnectionAsync()
ConstructHttp2ConnectionAsync()
HandleHttp2ConnectionFailure(System.Net.Http.HttpConnectionWaiter`1<System.Net.Http.Http2Connection>,System.Exception)
HandleHttp11Downgrade()
ReturnHttp2Connection(System.Net.Http.Http2Connection,System.Boolean,System.Net.Http.HttpConnectionWaiter`1<System.Net.Http.Http2Connection>)
DisableHttp2Connection(System.Net.Http.Http2Connection)
DisableHttp2ConnectionAsync()
InvalidateHttp2Connection(System.Net.Http.Http2Connection)
HeartBeat()
ScavengeHttp2ConnectionList(System.Collections.Generic.List`1<System.Net.Http.Http2Connection>,System.Collections.Generic.List`1<System.Net.Http.HttpConnectionBase>&,System.Int64,System.TimeSpan,System.TimeSpan)
.ctor(System.Net.Http.HttpConnectionPoolManager,System.Net.Http.HttpConnectionKind,System.String,System.Int32,System.String,System.Uri,System.String)
EnableMultipleHttp3Connections()
TrySendUsingHttp3Async()
TryGetPooledHttp3Connection(System.Net.Http.HttpRequestMessage,System.Net.Http.Http3Connection&,System.Net.Http.HttpConnectionWaiter`1<System.Net.Http.Http3Connection>&,System.Boolean&)
CheckForHttp3ConnectionInjection()
InjectNewHttp3ConnectionAsync()
HandleHttp3ConnectionFailure(System.Net.Http.HttpConnectionWaiter`1<System.Net.Http.Http3Connection>,System.Exception)
ReturnHttp3Connection(System.Net.Http.Http3Connection,System.Boolean,System.Net.Http.HttpConnectionWaiter`1<System.Net.Http.Http3Connection>)
DisableHttp3Connection(System.Net.Http.Http3Connection)
DisableHttp3ConnectionAsync()
InvalidateHttp3Connection(System.Net.Http.Http3Connection,System.Boolean)
ScavengeHttp3ConnectionList(System.Collections.Generic.List`1<System.Net.Http.Http3Connection>,System.Collections.Generic.List`1<System.Net.Http.HttpConnectionBase>&,System.Int64,System.TimeSpan,System.TimeSpan)
TryGetHttp3Authority(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpAuthority&,System.Exception&)
ProcessAltSvc(System.Net.Http.HttpResponseMessage)
HandleAltSvc(System.Collections.Generic.IEnumerable`1<System.String>,System.Nullable`1<System.TimeSpan>)
ExpireAltSvcAuthority()
IsAltSvcBlocked(System.Net.Http.HttpAuthority,System.Exception&)
BlocklistAuthority(System.Net.Http.HttpAuthority,System.Exception)
OnNetworkChanged()