< Summary

Information
Class: System.Net.Http.WaitForHttp3ConnectionActivity
Assembly: System.Net.Http
File(s): D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\Http3Connection.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 33
Coverable lines: 33
Total lines: 992
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)

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