< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 33
Coverable lines: 33
Total lines: 997
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 18
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)100%110%
Start()0%660%
Stop(...)0%10100%
AssertActivityNotRunning()0%220%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.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.Globalization;
 7using System.IO;
 8using System.Net.Http.Headers;
 9using System.Net.Http.Metrics;
 10using System.Net.Quic;
 11using System.Runtime.CompilerServices;
 12using System.Runtime.Versioning;
 13using System.Threading;
 14using System.Threading.Tasks;
 15
 16namespace System.Net.Http
 17{
 18    [SupportedOSPlatform("linux")]
 19    [SupportedOSPlatform("macos")]
 20    [SupportedOSPlatform("windows")]
 21    internal sealed class Http3Connection : HttpConnectionBase
 22    {
 23        private readonly HttpAuthority _authority;
 24        private readonly byte[]? _altUsedEncodedHeader;
 25        private QuicConnection? _connection;
 26        private Task? _connectionClosedTask;
 27
 28        // Keep a collection of requests around so we can process GOAWAY.
 29        private readonly Dictionary<QuicStream, Http3RequestStream> _activeRequests = new Dictionary<QuicStream, Http3Re
 30
 31        // Set when GOAWAY is being processed, when aborting, or when disposing.
 32        private long _firstRejectedStreamId = -1;
 33
 34        // Our control stream.
 35        private QuicStream? _clientControl;
 36        private Task? _sendSettingsTask;
 37
 38        // Server-advertised SETTINGS_MAX_FIELD_SECTION_SIZE
 39        // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1-2.2.1
 40        private uint _maxHeaderListSize = uint.MaxValue; // Defaults to infinite
 41
 42        // Once the server's streams are received, these are set to true. Further receipt of these streams results in a 
 43        private bool _haveServerControlStream;
 44        private bool _haveServerQpackDecodeStream;
 45        private bool _haveServerQpackEncodeStream;
 46
 47        // A connection-level error will abort any future operations.
 48        private Exception? _abortException;
 49
 50        public HttpAuthority Authority => _authority;
 51        public HttpConnectionPool Pool => _pool;
 52        public uint MaxHeaderListSize => _maxHeaderListSize;
 53        public byte[]? AltUsedEncodedHeaderBytes => _altUsedEncodedHeader;
 54        public Exception? AbortException => Volatile.Read(ref _abortException);
 55        private object SyncObj => _activeRequests;
 56
 57        private int _availableRequestStreamsCount;
 58        private TaskCompletionSource<bool>? _availableStreamsWaiter;
 59
 60        /// <summary>
 61        /// If true, we've received GOAWAY, are aborting due to a connection-level error, or are disposing due to pool l
 62        /// </summary>
 63        private bool ShuttingDown
 64        {
 65            get
 66            {
 67                Debug.Assert(Monitor.IsEntered(SyncObj));
 68                return _firstRejectedStreamId != -1;
 69            }
 70        }
 71
 72        public Http3Connection(HttpConnectionPool pool, HttpAuthority authority, bool includeAltUsedHeader)
 73            : base(pool, GetNextConnectionId())
 74        {
 75            _authority = authority;
 76
 77            if (includeAltUsedHeader)
 78            {
 79                bool altUsedDefaultPort = pool.Kind == HttpConnectionKind.Http && authority.Port == HttpConnectionPool.D
 80                string altUsedValue = altUsedDefaultPort ? authority.IdnHost : string.Create(CultureInfo.InvariantCultur
 81                _altUsedEncodedHeader = QPack.QPackEncoder.EncodeLiteralHeaderFieldWithoutNameReferenceToArray(KnownHead
 82            }
 83
 84            uint maxHeaderListSize = _pool._lastSeenHttp3MaxHeaderListSize;
 85            if (maxHeaderListSize > 0)
 86            {
 87                // Previous connections to the same host advertised a limit.
 88                // Use this as an initial value before we receive the SETTINGS frame.
 89                _maxHeaderListSize = maxHeaderListSize;
 90            }
 91        }
 92
 93        public void InitQuicConnection(QuicConnection connection, Activity? connectionSetupActivity, DnsEndPoint connect
 94        {
 95            // Report the exact DnsEndPoint used to establish the QUIC connection (Alt-Svc may point it at an authority
 96            // distinct from the pool's origin), consistent with the connection's RemoteEndPoint.
 97            MarkConnectionAsEstablished(connectionSetupActivity: connectionSetupActivity, remoteEndPoint: connection.Rem
 98
 99            _connection = connection;
 100
 101            // Avoid capturing the initial request's ExecutionContext for the entire lifetime of the new connection.
 102            using (ExecutionContext.SuppressFlow())
 103            {
 104                // Errors are observed via Abort().
 105                _sendSettingsTask = SendSettingsAsync();
 106
 107                // This process is cleaned up when _connection is disposed, and errors are observed via Abort().
 108                _ = AcceptStreamsAsync();
 109            }
 110        }
 111
 112        /// <summary>
 113        /// Starts shutting down the <see cref="Http3Connection"/>. Final cleanup will happen when there are no more act
 114        /// </summary>
 115        public override void Dispose()
 116        {
 117            lock (SyncObj)
 118            {
 119                if (_firstRejectedStreamId == -1)
 120                {
 121                    _firstRejectedStreamId = long.MaxValue;
 122                    CheckForShutdown();
 123                }
 124            }
 125        }
 126
 127        /// <summary>
 128        /// Called when shutting down, this checks for when shutdown is complete (no more active requests) and does actu
 129        /// </summary>
 130        /// <remarks>Requires <see cref="SyncObj"/> to be locked.</remarks>
 131        private void CheckForShutdown()
 132        {
 133            Debug.Assert(Monitor.IsEntered(SyncObj));
 134            Debug.Assert(ShuttingDown);
 135
 136            if (_activeRequests.Count != 0)
 137            {
 138                return;
 139            }
 140
 141            if (_connection != null)
 142            {
 143                // Close the QuicConnection in the background.
 144
 145                _availableStreamsWaiter?.SetResult(false);
 146                _availableStreamsWaiter = null;
 147
 148                _connectionClosedTask ??= _connection.CloseAsync((long)Http3ErrorCode.NoError).AsTask();
 149
 150                QuicConnection connection = _connection;
 151                _connection = null;
 152
 153                _ = _connectionClosedTask.ContinueWith(async closeTask =>
 154                {
 155                    if (closeTask.IsFaulted && NetEventSource.Log.IsEnabled())
 156                    {
 157                        Trace($"{nameof(QuicConnection)} failed to close: {closeTask.Exception!.InnerException}");
 158                    }
 159
 160                    try
 161                    {
 162                        await connection.DisposeAsync().ConfigureAwait(false);
 163                    }
 164                    catch (Exception ex)
 165                    {
 166                        Trace($"{nameof(QuicConnection)} failed to dispose: {ex}");
 167                    }
 168
 169                    if (_clientControl != null)
 170                    {
 171                        await _sendSettingsTask!.ConfigureAwait(false);
 172                        await _clientControl.DisposeAsync().ConfigureAwait(false);
 173                        _clientControl = null;
 174                    }
 175
 176                }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
 177
 178                MarkConnectionAsClosed();
 179            }
 180        }
 181
 182        /// <summary>
 183        /// When EnableMultipleHttp3Connections is false: always reserve a stream, return a bool indicating if the strea
 184        /// When EnableMultipleHttp3Connections is true: reserve a stream only if it's available meaning that the return
 185        /// </summary>
 186        public bool TryReserveStream()
 187        {
 188            bool singleConnection = !_pool.Settings.EnableMultipleHttp3Connections;
 189
 190            lock (SyncObj)
 191            {
 192                // For the single connection case, we allow the counter to go below zero.
 193                Debug.Assert(singleConnection || _availableRequestStreamsCount >= 0);
 194
 195                if (NetEventSource.Log.IsEnabled()) Trace($"_availableRequestStreamsCount = {_availableRequestStreamsCou
 196
 197                bool streamAvailable = _availableRequestStreamsCount > 0;
 198
 199                // Do not let the counter to go below zero when EnableMultipleHttp3Connections is true.
 200                // This equivalent to an immediate ReleaseStream() for the case no stream is immediately available.
 201                if (singleConnection || _availableRequestStreamsCount > 0)
 202                {
 203                    --_availableRequestStreamsCount;
 204                }
 205
 206                return streamAvailable;
 207            }
 208        }
 209
 210        public void ReleaseStream()
 211        {
 212            lock (SyncObj)
 213            {
 214                Debug.Assert(!_pool.Settings.EnableMultipleHttp3Connections || _availableRequestStreamsCount >= 0);
 215
 216                if (NetEventSource.Log.IsEnabled()) Trace($"_availableRequestStreamsCount = {_availableRequestStreamsCou
 217                ++_availableRequestStreamsCount;
 218
 219                _availableStreamsWaiter?.SetResult(!ShuttingDown);
 220                _availableStreamsWaiter = null;
 221            }
 222        }
 223
 224        public void StreamCapacityCallback(QuicConnection connection, QuicStreamCapacityChangedArgs args)
 225        {
 226            Debug.Assert(_connection is null || connection == _connection);
 227
 228            lock (SyncObj)
 229            {
 230                Debug.Assert(_availableStreamsWaiter is null || _availableRequestStreamsCount >= 0);
 231
 232                if (NetEventSource.Log.IsEnabled()) Trace($"_availableRequestStreamsCount = {_availableRequestStreamsCou
 233
 234                // Since _availableStreamsWaiter is only used in the multi-connection case, when _availableRequestStream
 235                // we don't need to check the value of _availableRequestStreamsCount here.
 236                _availableRequestStreamsCount += args.BidirectionalIncrement;
 237                _availableStreamsWaiter?.SetResult(!ShuttingDown);
 238                _availableStreamsWaiter = null;
 239            }
 240        }
 241
 242        public Task<bool> WaitForAvailableStreamsAsync()
 243        {
 244            // In the single connection case, _availableStreamsWaiter notifications do not guarantee that _availableRequ
 245            Debug.Assert(_pool.Settings.EnableMultipleHttp3Connections, "Calling WaitForAvailableStreamsAsync() is inval
 246
 247            lock (SyncObj)
 248            {
 249                Debug.Assert(_availableRequestStreamsCount >= 0);
 250
 251                if (ShuttingDown)
 252                {
 253                    return Task.FromResult(false);
 254                }
 255                if (_availableRequestStreamsCount > 0)
 256                {
 257                    return Task.FromResult(true);
 258                }
 259
 260                Debug.Assert(_availableStreamsWaiter is null);
 261                _availableStreamsWaiter = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronou
 262                return _availableStreamsWaiter.Task;
 263            }
 264        }
 265
 266        public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, WaitForHttp3ConnectionActivity wait
 267        {
 268            request.ConnectionId = Id;
 269
 270            // Allocate an active request
 271            QuicStream? quicStream = null;
 272            Http3RequestStream? requestStream = null;
 273
 274            try
 275            {
 276                Exception? exception = null;
 277                try
 278                {
 279                    QuicConnection? conn = _connection;
 280                    if (conn != null)
 281                    {
 282                        // We found a connection in the pool, but it did not have available streams, OpenOutboundStreamA
 283                        if (!waitForConnectionActivity.Started && !streamAvailable)
 284                        {
 285                            waitForConnectionActivity.Start();
 286                        }
 287
 288                        quicStream = await conn.OpenOutboundStreamAsync(QuicStreamType.Bidirectional, cancellationToken)
 289
 290                        requestStream = new Http3RequestStream(request, this, quicStream);
 291                        lock (SyncObj)
 292                        {
 293                            if (_activeRequests.Count == 0)
 294                            {
 295                                MarkConnectionAsNotIdle();
 296                            }
 297                            _activeRequests.Add(quicStream, requestStream);
 298                        }
 299                    }
 300                }
 301                // Swallow any exceptions caused by the connection being closed locally or even disposed due to a race.
 302                // Since quicStream will stay `null`, the code below will throw appropriate exception to retry the reque
 303                catch (ObjectDisposedException e)
 304                {
 305                    exception = e;
 306                }
 307                catch (QuicException e) when (e.QuicError != QuicError.OperationAborted)
 308                {
 309                    exception = e;
 310                }
 311                finally
 312                {
 313                    waitForConnectionActivity.Stop(request, Pool, exception);
 314                }
 315
 316                if (quicStream == null)
 317                {
 318                    throw new HttpRequestException(HttpRequestError.Unknown, SR.net_http_request_aborted, null, RequestR
 319                }
 320
 321                requestStream!.StreamId = quicStream.Id;
 322
 323                bool goAway;
 324                lock (SyncObj)
 325                {
 326                    goAway = _firstRejectedStreamId != -1 && requestStream.StreamId >= _firstRejectedStreamId;
 327                }
 328
 329                if (goAway)
 330                {
 331                    throw new HttpRequestException(HttpRequestError.Unknown, SR.net_http_request_aborted, null, RequestR
 332                }
 333
 334                waitForConnectionActivity.AssertActivityNotRunning();
 335                if (ConnectionSetupActivity is not null) ConnectionSetupDistributedTracing.AddConnectionLinkToRequestAct
 336                if (NetEventSource.Log.IsEnabled()) Trace($"Sending request: {request}");
 337
 338                Task<HttpResponseMessage> responseTask = requestStream.SendAsync(cancellationToken);
 339
 340                // null out requestStream to avoid disposing in finally block. It is now in charge of disposing itself.
 341                requestStream = null;
 342
 343                return await responseTask.ConfigureAwait(false);
 344            }
 345            catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted)
 346            {
 347                // This will happen if we aborted _connection somewhere and we have pending OpenOutboundStreamAsync call
 348                // note that _abortException may be null if we closed the connection in response to a GOAWAY frame
 349                throw new HttpRequestException(HttpRequestError.Unknown, SR.net_http_client_execution_error, _abortExcep
 350            }
 351            finally
 352            {
 353                if (requestStream is not null)
 354                {
 355                    await requestStream.DisposeAsync().ConfigureAwait(false);
 356                }
 357            }
 358        }
 359
 360        /// <summary>
 361        /// Aborts the connection with an error.
 362        /// </summary>
 363        /// <remarks>
 364        /// Used for e.g. I/O or connection-level frame parsing errors.
 365        /// </remarks>
 366        internal Exception Abort(Exception abortException)
 367        {
 368            // Only observe the first exception we get.
 369            Exception? firstException = Interlocked.CompareExchange(ref _abortException, abortException, null);
 370
 371            if (firstException != null)
 372            {
 373                if (NetEventSource.Log.IsEnabled() && !ReferenceEquals(firstException, abortException))
 374                {
 375                    // Lost the race to set the field to another exception, so just trace this one.
 376                    Trace($"{nameof(abortException)}=={abortException}");
 377                }
 378
 379                return firstException;
 380            }
 381
 382            // Stop sending requests to this connection.
 383            // Do not dispose the connection when invalidating as the rest of this method does exactly that:
 384            //   set up _firstRejectedStreamId, close the connection with proper error code and CheckForShutdown.
 385            _pool.InvalidateHttp3Connection(this, dispose: false);
 386
 387            long connectionResetErrorCode = (abortException as HttpProtocolException)?.ErrorCode ?? (long)Http3ErrorCode
 388
 389            lock (SyncObj)
 390            {
 391                // Set _firstRejectedStreamId != -1 to make ShuttingDown = true.
 392                // It's possible GOAWAY is already being processed, in which case this would already be != -1.
 393                if (_firstRejectedStreamId == -1)
 394                {
 395                    _firstRejectedStreamId = long.MaxValue;
 396                }
 397
 398                // Abort the connection. This will cause all of our streams to abort on their next I/O.
 399                if (_connection != null && _connectionClosedTask == null)
 400                {
 401                    _connectionClosedTask = _connection.CloseAsync((long)connectionResetErrorCode).AsTask();
 402                }
 403
 404                CheckForShutdown();
 405            }
 406
 407            return abortException;
 408        }
 409
 410        private void OnServerGoAway(long firstRejectedStreamId)
 411        {
 412            if (NetEventSource.Log.IsEnabled())
 413            {
 414                Trace($"GOAWAY received. First rejected stream ID = {firstRejectedStreamId}");
 415            }
 416
 417            // Stop sending requests to this connection.
 418            // Do not dispose the connection when invalidating as the rest of this method does exactly that:
 419            //   set up _firstRejectedStreamId to the stream id from GO_AWAY frame and CheckForShutdown.
 420            _pool.InvalidateHttp3Connection(this, dispose: false);
 421
 422            var streamsToGoAway = new List<Http3RequestStream>();
 423
 424            lock (SyncObj)
 425            {
 426                if (_firstRejectedStreamId != -1 && firstRejectedStreamId > _firstRejectedStreamId)
 427                {
 428                    // Server can send multiple GOAWAY frames.
 429                    // Spec says a server MUST NOT increase the stream ID in subsequent GOAWAYs,
 430                    // but doesn't specify what client should do if that is violated. Ignore for now.
 431                    if (NetEventSource.Log.IsEnabled())
 432                    {
 433                        Trace("HTTP/3 server sent GOAWAY with increasing stream ID. Retried requests may have been doubl
 434                    }
 435                    return;
 436                }
 437
 438                _firstRejectedStreamId = firstRejectedStreamId;
 439
 440                foreach (KeyValuePair<QuicStream, Http3RequestStream> request in _activeRequests)
 441                {
 442                    if (request.Value.StreamId >= firstRejectedStreamId)
 443                    {
 444                        streamsToGoAway.Add(request.Value);
 445                    }
 446                }
 447
 448                CheckForShutdown();
 449            }
 450
 451            // GOAWAY each stream outside of the lock, so they can acquire the lock to remove themselves from _activeReq
 452            foreach (Http3RequestStream stream in streamsToGoAway)
 453            {
 454                stream.GoAway();
 455            }
 456        }
 457
 458        public void RemoveStream(QuicStream stream)
 459        {
 460            lock (SyncObj)
 461            {
 462                if (_activeRequests.Remove(stream))
 463                {
 464                    if (_activeRequests.Count == 0)
 465                    {
 466                        MarkConnectionAsIdle();
 467                    }
 468
 469                    if (ShuttingDown)
 470                    {
 471                        CheckForShutdown();
 472                    }
 473                }
 474            }
 475        }
 476
 477        public override void Trace(string message, [CallerMemberName] string? memberName = null) =>
 478            Trace(0, _connection is not null ? $"{_connection} {message}" : message, memberName);
 479
 480        internal void Trace(long streamId, string message, [CallerMemberName] string? memberName = null) =>
 481            NetEventSource.Log.HandlerMessage(
 482                _pool?.GetHashCode() ?? 0,    // pool ID
 483                GetHashCode(),                // connection ID
 484                (int)streamId,                // stream ID
 485                memberName,                   // method name
 486                message);                     // message
 487
 488        private async Task SendSettingsAsync()
 489        {
 490            try
 491            {
 492                _clientControl = await _connection!.OpenOutboundStreamAsync(QuicStreamType.Unidirectional).ConfigureAwai
 493
 494                // Server MUST NOT abort our control stream, setup a continuation which will react accordingly
 495                _ = _clientControl.WritesClosed.ContinueWith(t =>
 496                {
 497                    if (t.Exception?.InnerException is QuicException ex && ex.QuicError == QuicError.StreamAborted)
 498                    {
 499                        Abort(HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.ClosedCriticalStream))
 500                    }
 501                }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Current);
 502
 503                await _clientControl.WriteAsync(_pool.Settings.Http3SettingsFrame, CancellationToken.None).ConfigureAwai
 504            }
 505            catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted)
 506            {
 507                Debug.Assert(ex.ApplicationErrorCode.HasValue);
 508                Http3ErrorCode code = (Http3ErrorCode)ex.ApplicationErrorCode.Value;
 509
 510                Abort(HttpProtocolException.CreateHttp3ConnectionException(code, SR.net_http_http3_connection_close));
 511            }
 512            catch (Exception ex)
 513            {
 514                Abort(ex);
 515            }
 516        }
 517
 518        public static unsafe byte[] BuildSettingsFrame(HttpConnectionSettings settings)
 519        {
 520            Span<byte> buffer = stackalloc byte[4 + VariableLengthIntegerHelper.MaximumEncodedLength];
 521
 522            int integerLength = VariableLengthIntegerHelper.WriteInteger(buffer.Slice(4), settings.MaxResponseHeadersByt
 523            int payloadLength = 1 + integerLength; // includes the setting ID and the integer value.
 524            Debug.Assert(payloadLength <= VariableLengthIntegerHelper.OneByteLimit);
 525
 526            buffer[0] = (byte)Http3StreamType.Control;
 527            buffer[1] = (byte)Http3FrameType.Settings;
 528            buffer[2] = (byte)payloadLength;
 529            buffer[3] = (byte)Http3SettingType.MaxHeaderListSize;
 530
 531            return buffer.Slice(0, 4 + integerLength).ToArray();
 532        }
 533
 534        /// <summary>
 535        /// Accepts unidirectional streams (control, QPack, ...) from the server.
 536        /// </summary>
 537        private async Task AcceptStreamsAsync()
 538        {
 539            try
 540            {
 541                while (true)
 542                {
 543                    ValueTask<QuicStream> streamTask;
 544
 545                    lock (SyncObj)
 546                    {
 547                        if (ShuttingDown)
 548                        {
 549                            return;
 550                        }
 551
 552                        // No cancellation token is needed here; we expect the operation to cancel itself when _connecti
 553                        streamTask = _connection!.AcceptInboundStreamAsync(CancellationToken.None);
 554                    }
 555
 556                    QuicStream stream = await streamTask.ConfigureAwait(false);
 557
 558                    // This process is cleaned up when _connection is disposed, and errors are observed via Abort().
 559                    _ = ProcessServerStreamAsync(stream);
 560                }
 561            }
 562            catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted)
 563            {
 564                // Shutdown initiated by us, no need to abort.
 565            }
 566            catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted)
 567            {
 568                Debug.Assert(ex.ApplicationErrorCode.HasValue);
 569                Http3ErrorCode code = (Http3ErrorCode)ex.ApplicationErrorCode.Value;
 570
 571                Abort(HttpProtocolException.CreateHttp3ConnectionException(code, SR.net_http_http3_connection_close));
 572            }
 573            catch (Exception ex)
 574            {
 575                Abort(ex);
 576            }
 577        }
 578
 579        /// <summary>
 580        /// Routes a stream to an appropriate stream-type-specific processor
 581        /// </summary>
 582        private async Task ProcessServerStreamAsync(QuicStream stream)
 583        {
 584            ArrayBuffer buffer = default;
 585
 586            try
 587            {
 588                await using (stream.ConfigureAwait(false))
 589                {
 590                    // Check if this is a bidirectional stream (which we don't support from the server).
 591                    if (stream.CanWrite)
 592                    {
 593                        // Server initiated bidirectional streams are either push streams or extensions, and we support 
 594                        throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.StreamCreationError);
 595                    }
 596
 597                    buffer = new ArrayBuffer(initialSize: 32, usePool: true);
 598
 599                    // Read the stream type, which is a variable-length integer.
 600                    // This may require multiple reads if the integer is encoded in multiple bytes.
 601                    long streamType;
 602                    while (true)
 603                    {
 604                        int bytesRead;
 605                        try
 606                        {
 607                            bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).Configure
 608                        }
 609                        catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted)
 610                        {
 611                            // Treat identical to receiving 0. See below comment.
 612                            bytesRead = 0;
 613                        }
 614
 615                        if (bytesRead == 0)
 616                        {
 617                            // https://www.rfc-editor.org/rfc/rfc9114.html#name-unidirectional-streams
 618                            // A sender can close or reset a unidirectional stream unless otherwise specified. A receive
 619                            // tolerate unidirectional streams being closed or reset prior to the reception of the unidi
 620                            // stream header.
 621                            return;
 622                        }
 623
 624                        buffer.Commit(bytesRead);
 625
 626                        if (VariableLengthIntegerHelper.TryRead(buffer.ActiveSpan, out streamType, out int streamTypeLen
 627                        {
 628                            // Successfully read the stream type.
 629                            buffer.Discard(streamTypeLength);
 630                            break;
 631                        }
 632                    }
 633
 634                    if (NetEventSource.Log.IsEnabled())
 635                    {
 636                        NetEventSource.Info(this, $"Received server-initiated unidirectional stream of type {streamType}
 637                    }
 638
 639                    // Process the stream based on its type.
 640                    switch ((Http3StreamType)streamType)
 641                    {
 642                        case Http3StreamType.Control:
 643                            if (Interlocked.Exchange(ref _haveServerControlStream, true))
 644                            {
 645                                // A second control stream has been received.
 646                                throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.StreamCreation
 647                            }
 648
 649                            // Ownership of buffer is transferred to ProcessServerControlStreamAsync.
 650                            ArrayBuffer bufferCopy = buffer;
 651                            buffer = default;
 652
 653                            await ProcessServerControlStreamAsync(stream, bufferCopy).ConfigureAwait(false);
 654                            return;
 655                        case Http3StreamType.QPackDecoder:
 656                            if (Interlocked.Exchange(ref _haveServerQpackDecodeStream, true))
 657                            {
 658                                // A second QPack decode stream has been received.
 659                                throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.StreamCreation
 660                            }
 661
 662                            // The stream must not be closed, but we aren't using QPACK right now -- ignore.
 663                            buffer.Dispose();
 664                            await stream.CopyToAsync(Stream.Null).ConfigureAwait(false);
 665                            return;
 666                        case Http3StreamType.QPackEncoder:
 667                            if (Interlocked.Exchange(ref _haveServerQpackEncodeStream, true))
 668                            {
 669                                // A second QPack encode stream has been received.
 670                                throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.StreamCreation
 671                            }
 672
 673                            // We haven't enabled QPack in our SETTINGS frame, so we shouldn't receive any meaningful da
 674                            // However, the standard says the stream must not be closed for the lifetime of the connecti
 675                            buffer.Dispose();
 676                            await stream.CopyToAsync(Stream.Null).ConfigureAwait(false);
 677                            return;
 678                        case Http3StreamType.Push:
 679                            // We don't support push streams.
 680                            // Because no maximum push stream ID was negotiated via a MAX_PUSH_ID frame, server should n
 681                            throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.IdError);
 682                        default:
 683                            // Unknown stream type. Per spec, these must be ignored and aborted but not be considered a 
 684                            stream.Abort(QuicAbortDirection.Read, (long)Http3ErrorCode.StreamCreationError);
 685                            return;
 686                    }
 687                }
 688            }
 689            catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted)
 690            {
 691                // ignore the exception, we have already closed the connection
 692            }
 693            catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted)
 694            {
 695                Debug.Assert(ex.ApplicationErrorCode.HasValue);
 696                Http3ErrorCode code = (Http3ErrorCode)ex.ApplicationErrorCode.Value;
 697
 698                Abort(HttpProtocolException.CreateHttp3ConnectionException(code, SR.net_http_http3_connection_close));
 699            }
 700            catch (Exception ex)
 701            {
 702                Abort(ex);
 703            }
 704            finally
 705            {
 706                buffer.Dispose();
 707            }
 708        }
 709
 710        /// <summary>
 711        /// Reads the server's control stream.
 712        /// </summary>
 713        private async Task ProcessServerControlStreamAsync(QuicStream stream, ArrayBuffer buffer)
 714        {
 715            try
 716            {
 717                using (buffer)
 718                {
 719                    // Read the first frame of the control stream. Per spec:
 720                    // A SETTINGS frame MUST be sent as the first frame of each control stream.
 721
 722                    (Http3FrameType? frameType, long payloadLength) = await ReadFrameEnvelopeAsync().ConfigureAwait(fals
 723
 724                    if (frameType == null)
 725                    {
 726                        // Connection closed prematurely, expected SETTINGS frame.
 727                        throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.ClosedCriticalStream);
 728                    }
 729
 730                    if (frameType != Http3FrameType.Settings)
 731                    {
 732                        throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.MissingSettings);
 733                    }
 734
 735                    await ProcessSettingsFrameAsync(payloadLength).ConfigureAwait(false);
 736
 737                    // Read subsequent frames.
 738
 739                    while (true)
 740                    {
 741                        (frameType, payloadLength) = await ReadFrameEnvelopeAsync().ConfigureAwait(false);
 742
 743                        switch (frameType)
 744                        {
 745                            case Http3FrameType.GoAway:
 746                                await ProcessGoAwayFrameAsync(payloadLength).ConfigureAwait(false);
 747                                break;
 748                            case Http3FrameType.Settings:
 749                                // If an endpoint receives a second SETTINGS frame on the control stream, the endpoint M
 750                                throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.UnexpectedFram
 751                            case Http3FrameType.Headers: // Servers should not send these frames to a control stream.
 752                            case Http3FrameType.Data:
 753                            case Http3FrameType.MaxPushId:
 754                            case Http3FrameType.ReservedHttp2Priority: // These frames are explicitly reserved and must 
 755                            case Http3FrameType.ReservedHttp2Ping:
 756                            case Http3FrameType.ReservedHttp2WindowUpdate:
 757                            case Http3FrameType.ReservedHttp2Continuation:
 758                                if (NetEventSource.Log.IsEnabled())
 759                                {
 760                                    Trace($"Received reserved frame: {frameType}");
 761                                }
 762                                throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.UnexpectedFram
 763                            case Http3FrameType.PushPromise:
 764                            case Http3FrameType.CancelPush:
 765                                // Because we haven't sent any MAX_PUSH_ID frame, it is invalid to receive any push-rela
 766                                throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.IdError);
 767                            case null:
 768                                // End of stream reached. If we're shutting down, stop looping. Otherwise, this is an er
 769                                bool shuttingDown;
 770                                lock (SyncObj)
 771                                {
 772                                    shuttingDown = ShuttingDown;
 773                                }
 774                                if (!shuttingDown)
 775                                {
 776                                    if (NetEventSource.Log.IsEnabled())
 777                                    {
 778                                        Trace($"Control stream closed by the server.");
 779                                    }
 780                                    throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.ClosedCrit
 781                                }
 782                                return;
 783                            default:
 784                                await SkipUnknownPayloadAsync(payloadLength).ConfigureAwait(false);
 785                                break;
 786                        }
 787                    }
 788                }
 789            }
 790            catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted)
 791            {
 792                // Peers MUST NOT close the control stream
 793                throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.ClosedCriticalStream);
 794            }
 795
 796            async ValueTask<(Http3FrameType? frameType, long payloadLength)> ReadFrameEnvelopeAsync()
 797            {
 798                long frameType, payloadLength;
 799                int bytesRead;
 800
 801                while (!Http3Frame.TryReadIntegerPair(buffer.ActiveSpan, out frameType, out payloadLength, out bytesRead
 802                {
 803                    buffer.EnsureAvailableSpace(VariableLengthIntegerHelper.MaximumEncodedLength * 2);
 804                    bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(fa
 805
 806                    if (bytesRead != 0)
 807                    {
 808                        buffer.Commit(bytesRead);
 809                    }
 810                    else if (buffer.ActiveLength == 0)
 811                    {
 812                        // End of stream.
 813                        return (null, 0);
 814                    }
 815                    else
 816                    {
 817                        // Our buffer has partial frame data in it but not enough to complete the read: bail out.
 818                        throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError);
 819                    }
 820                }
 821
 822                buffer.Discard(bytesRead);
 823
 824                return ((Http3FrameType)frameType, payloadLength);
 825            }
 826
 827            async ValueTask ProcessSettingsFrameAsync(long settingsPayloadLength)
 828            {
 829                while (settingsPayloadLength != 0)
 830                {
 831                    long settingId, settingValue;
 832                    int bytesRead;
 833
 834                    while (!Http3Frame.TryReadIntegerPair(buffer.ActiveSpan, out settingId, out settingValue, out bytesR
 835                    {
 836                        buffer.EnsureAvailableSpace(VariableLengthIntegerHelper.MaximumEncodedLength * 2);
 837                        bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwai
 838
 839                        if (bytesRead != 0)
 840                        {
 841                            buffer.Commit(bytesRead);
 842                        }
 843                        else
 844                        {
 845                            // Our buffer has partial frame data in it but not enough to complete the read: bail out.
 846                            throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError);
 847                        }
 848                    }
 849
 850                    settingsPayloadLength -= bytesRead;
 851
 852                    if (settingsPayloadLength < 0)
 853                    {
 854                        // An integer was encoded past the payload length.
 855                        // A frame payload that contains additional bytes after the identified fields or a frame payload
 856                        throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError);
 857                    }
 858
 859                    buffer.Discard(bytesRead);
 860
 861                    if (NetEventSource.Log.IsEnabled()) Trace($"Applying setting {(Http3SettingType)settingId}={settingV
 862
 863                    switch ((Http3SettingType)settingId)
 864                    {
 865                        case Http3SettingType.MaxHeaderListSize:
 866                            _maxHeaderListSize = (uint)Math.Min((ulong)settingValue, uint.MaxValue);
 867                            _pool._lastSeenHttp3MaxHeaderListSize = _maxHeaderListSize;
 868                            break;
 869                        case Http3SettingType.ReservedHttp2EnablePush:
 870                        case Http3SettingType.ReservedHttp2MaxConcurrentStreams:
 871                        case Http3SettingType.ReservedHttp2InitialWindowSize:
 872                        case Http3SettingType.ReservedHttp2MaxFrameSize:
 873                            // Per https://tools.ietf.org/html/draft-ietf-quic-http-31#section-7.2.4.1
 874                            // these settings IDs are reserved and must never be sent.
 875                            throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.SettingsError);
 876                    }
 877                }
 878            }
 879
 880            async ValueTask ProcessGoAwayFrameAsync(long goawayPayloadLength)
 881            {
 882                long firstRejectedStreamId;
 883                int bytesRead;
 884
 885                while (!VariableLengthIntegerHelper.TryRead(buffer.ActiveSpan, out firstRejectedStreamId, out bytesRead)
 886                {
 887                    buffer.EnsureAvailableSpace(VariableLengthIntegerHelper.MaximumEncodedLength);
 888                    bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(fa
 889
 890                    if (bytesRead != 0)
 891                    {
 892                        buffer.Commit(bytesRead);
 893                    }
 894                    else
 895                    {
 896                        // Our buffer has partial frame data in it but not enough to complete the read: bail out.
 897                        throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError);
 898                    }
 899                }
 900
 901                buffer.Discard(bytesRead);
 902                if (bytesRead != goawayPayloadLength)
 903                {
 904                    // Frame contains unknown extra data after the integer.
 905                    throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError);
 906                }
 907
 908                OnServerGoAway(firstRejectedStreamId);
 909            }
 910
 911            async ValueTask SkipUnknownPayloadAsync(long payloadLength)
 912            {
 913                while (payloadLength != 0)
 914                {
 915                    if (buffer.ActiveLength == 0)
 916                    {
 917                        int bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).Configure
 918
 919                        if (bytesRead != 0)
 920                        {
 921                            buffer.Commit(bytesRead);
 922                        }
 923                        else
 924                        {
 925                            // Our buffer has partial frame data in it but not enough to complete the read: bail out.
 926                            throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError);
 927                        }
 928                    }
 929
 930                    long readLength = Math.Min(payloadLength, buffer.ActiveLength);
 931                    buffer.Discard((int)readLength);
 932                    payloadLength -= readLength;
 933                }
 934            }
 935        }
 936    }
 937
 938    /// <summary>
 939    /// Tracks telemetry signals associated with the time period an HTTP/3 request spends waiting for a usable HTTP/3 co
 940    /// the wait_for_connection Activity, the RequestLeftQueue EventSource event and the http.client.request.time_in_que
 941    /// </summary>
 942    internal struct WaitForHttp3ConnectionActivity
 943    {
 944        // The HttpConnectionSettings -> SocketsHttpHandlerMetrics indirection is needed for the trimmer.
 945        private HttpConnectionSettings _settings;
 946        private readonly HttpAuthority _authority;
 947        private Activity? _activity;
 948        private long _startTimestamp;
 949
 950        public WaitForHttp3ConnectionActivity(HttpConnectionSettings settings, HttpAuthority authority)
 0951        {
 0952            _settings = settings;
 0953            _authority = authority;
 0954        }
 955
 0956        public bool Started { get; private set; }
 957
 958        public void Start()
 0959        {
 0960            Debug.Assert(!Started);
 0961            _startTimestamp = HttpTelemetry.Log.IsEnabled() || (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled && _
 0962            _activity = ConnectionSetupDistributedTracing.StartWaitForConnectionActivity(_authority);
 0963            Started = true;
 0964        }
 965
 966        public void Stop(HttpRequestMessage request, HttpConnectionPool pool, Exception? exception)
 0967        {
 0968            if (exception is not null)
 0969            {
 0970                ConnectionSetupDistributedTracing.ReportError(_activity, exception);
 0971            }
 972
 0973            _activity?.Stop();
 974
 0975            if (_startTimestamp != 0)
 0976            {
 0977                TimeSpan duration = Stopwatch.GetElapsedTime(_startTimestamp);
 978
 0979                if (GlobalHttpSettings.MetricsHandler.IsGloballyEnabled)
 0980                {
 0981                    _settings._metrics!.RequestLeftQueue(request, pool, duration, versionMajor: 3);
 0982                }
 0983                if (HttpTelemetry.Log.IsEnabled())
 0984                {
 0985                    HttpTelemetry.Log.RequestLeftQueue(3, duration);
 0986                }
 0987            }
 0988        }
 989
 990        [Conditional("DEBUG")]
 991        public void AssertActivityNotRunning()
 0992        {
 0993            Debug.Assert(_activity?.IsStopped != false);
 0994        }
 995    }
 996}
 997