Ejemplo n.º 1
0
        private async ValueTask ProcessOutboundControlStreamAsync(Http3ControlStream controlStream)
        {
            try
            {
                await controlStream.SendStreamIdAsync(id : 0);

                await controlStream.SendSettingsFrameAsync();
            }
            catch (Exception ex)
            {
                Log.Http3OutboundControlStreamError(ConnectionId, ex);

                var connectionError = new Http3ConnectionErrorException(CoreStrings.Http3ControlStreamErrorInitializingOutbound, Http3ErrorCode.ClosedCriticalStream);
                Log.Http3ConnectionError(ConnectionId, connectionError);

                // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2.1
                Abort(new ConnectionAbortedException(connectionError.Message, connectionError), connectionError.ErrorCode);
            }
        }
Ejemplo n.º 2
0
        public async Task ProcessRequestsAsync <TContext>(IHttpApplication <TContext> application) where TContext : notnull
        {
            // An endpoint MAY avoid creating an encoder stream if it's not going to
            // be used(for example if its encoder doesn't wish to use the dynamic
            // table, or if the maximum size of the dynamic table permitted by the
            // peer is zero).

            // An endpoint MAY avoid creating a decoder stream if its decoder sets
            // the maximum capacity of the dynamic table to zero.

            // Don't create Encoder and Decoder as they aren't used now.

            Exception?         error = null;
            Http3ControlStream?outboundControlStream     = null;
            ValueTask          outboundControlStreamTask = default;
            bool clientAbort = false;

            try
            {
                outboundControlStream = await CreateNewUnidirectionalStreamAsync(application);

                lock (_sync)
                {
                    OutboundControlStream = outboundControlStream;
                }

                // Don't delay on waiting to send outbound control stream settings.
                outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream);

                while (_stoppedAcceptingStreams == 0)
                {
                    var streamContext = await _multiplexedContext.AcceptAsync(_acceptStreamsCts.Token);

                    try
                    {
                        // Return null on server close or cancellation.
                        if (streamContext == null)
                        {
                            if (_acceptStreamsCts.Token.IsCancellationRequested)
                            {
                                _acceptStreamsCts = new CancellationTokenSource();
                            }

                            // There is no stream so continue to skip to UpdateConnectionState in finally.
                            // UpdateConnectionState is responsible for updating connection to
                            // stop accepting streams and break out of accept loop.
                            continue;
                        }

                        var streamDirectionFeature = streamContext.Features.Get <IStreamDirectionFeature>();
                        var streamIdFeature        = streamContext.Features.Get <IStreamIdFeature>();

                        Debug.Assert(streamDirectionFeature != null);
                        Debug.Assert(streamIdFeature != null);

                        if (!streamDirectionFeature.CanWrite)
                        {
                            // Unidirectional stream
                            var stream = new Http3ControlStream <TContext>(application, CreateHttpStreamContext(streamContext));
                            _streamLifetimeHandler.OnStreamCreated(stream);

                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                        else
                        {
                            // Request stream

                            // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-5.2-2
                            if (_gracefulCloseStarted)
                            {
                                // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.2-3
                                streamContext.Features.Get <IProtocolErrorCodeFeature>() !.Error = (long)Http3ErrorCode.RequestRejected;
                                streamContext.Abort(new ConnectionAbortedException("HTTP/3 connection is closing and no longer accepts new requests."));
                                await streamContext.DisposeAsync();

                                continue;
                            }

                            // Request stream IDs are tracked.
                            UpdateHighestOpenedRequestStreamId(streamIdFeature.StreamId);

                            var persistentStateFeature = streamContext.Features.Get <IPersistentStateFeature>();
                            Debug.Assert(persistentStateFeature != null, $"Required {nameof(IPersistentStateFeature)} not on stream context.");

                            Http3Stream <TContext> stream;

                            // Check whether there is an existing HTTP/3 stream on the transport stream.
                            // A stream will only be cached if the transport stream itself is reused.
                            if (!persistentStateFeature.State.TryGetValue(StreamPersistentStateKey, out var s))
                            {
                                stream = new Http3Stream <TContext>(application, CreateHttpStreamContext(streamContext));
                                persistentStateFeature.State.Add(StreamPersistentStateKey, stream);
                            }
                            else
                            {
                                stream = (Http3Stream <TContext>)s !;
                                stream.InitializeWithExistingContext(streamContext.Transport);
                            }

                            _streamLifetimeHandler.OnStreamCreated(stream);

                            KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                    }
                    finally
                    {
                        UpdateConnectionState();
                    }
                }
            }
            catch (ConnectionResetException ex)
            {
                lock (_streams)
                {
                    if (_activeRequestCount > 0)
                    {
                        Log.RequestProcessingError(_context.ConnectionId, ex);
                    }
                }
                error       = ex;
                clientAbort = true;
            }
            catch (IOException ex)
            {
                Log.RequestProcessingError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (ConnectionAbortedException ex)
            {
                Log.RequestProcessingError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (Http3ConnectionErrorException ex)
            {
                Log.Http3ConnectionError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (Exception ex)
            {
                error = ex;
            }
            finally
            {
                try
                {
                    // Don't try to send GOAWAY if the client has already closed the connection.
                    if (!clientAbort)
                    {
                        if (TryStopAcceptingStreams() || _gracefulCloseStarted)
                        {
                            await SendGoAwayAsync(GetCurrentGoAwayStreamId());
                        }
                    }

                    // Abort active request streams.
                    lock (_streams)
                    {
                        foreach (var stream in _streams.Values)
                        {
                            stream.Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error);
                        }
                    }

                    if (outboundControlStream != null)
                    {
                        // Don't gracefully close the outbound control stream. If the peer detects
                        // the control stream closes it will close with a procotol error.
                        // Instead, allow control stream to be automatically aborted when the
                        // connection is aborted.
                        await outboundControlStreamTask;
                    }

                    // Complete
                    Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error);

                    // Wait for active requests to complete.
                    while (_activeRequestCount > 0)
                    {
                        await _streamCompletionAwaitable;
                    }

                    _context.TimeoutControl.CancelTimeout();
                }
                catch
                {
                    Abort(CreateConnectionAbortError(error, clientAbort), Http3ErrorCode.InternalError);
                    throw;
                }
                finally
                {
                    // Connection can close without processing any request streams.
                    var streamId = _highestOpenedRequestStreamId != DefaultHighestOpenedRequestStreamId
                        ? _highestOpenedRequestStreamId
                        : (long?)null;

                    Log.Http3ConnectionClosed(_context.ConnectionId, streamId);
                }
            }
        }
Ejemplo n.º 3
0
 public bool OnInboundEncoderStream(Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3ControlStream stream)
 {
     return(_inner.OnInboundEncoderStream(stream));
 }
Ejemplo n.º 4
0
        public async Task ProcessRequestsAsync <TContext>(IHttpApplication <TContext> application)
        {
            var streamListenerFeature = Context.ConnectionFeatures.Get <IQuicStreamListenerFeature>();

            // Start other three unidirectional streams here.
            var settingsStream = CreateSettingsStream(application);
            var encoderStream  = CreateEncoderStream(application);
            var decoderStream  = CreateDecoderStream(application);

            try
            {
                while (true)
                {
                    var connectionContext = await streamListenerFeature.AcceptAsync();

                    if (connectionContext == null)
                    {
                        break;
                    }

                    //if (_haveSentGoAway)
                    //{
                    //    // error here.
                    //}

                    var httpConnectionContext = new HttpConnectionContext
                    {
                        ConnectionId       = connectionContext.ConnectionId,
                        ConnectionContext  = connectionContext,
                        Protocols          = Context.Protocols,
                        ServiceContext     = Context.ServiceContext,
                        ConnectionFeatures = connectionContext.Features,
                        MemoryPool         = Context.MemoryPool,
                        Transport          = connectionContext.Transport,
                        TimeoutControl     = Context.TimeoutControl,
                        LocalEndPoint      = connectionContext.LocalEndPoint as IPEndPoint,
                        RemoteEndPoint     = connectionContext.RemoteEndPoint as IPEndPoint
                    };

                    var streamFeature = httpConnectionContext.ConnectionFeatures.Get <IQuicStreamFeature>();
                    var streamId      = streamFeature.StreamId;
                    HighestStreamId = streamId;

                    if (streamFeature.IsUnidirectional)
                    {
                        var stream = new Http3ControlStream <TContext>(application, this, httpConnectionContext);
                        ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                    }
                    else
                    {
                        var http3Stream = new Http3Stream <TContext>(application, this, httpConnectionContext);
                        var stream      = http3Stream;
                        _streams[streamId] = http3Stream;
                        ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                    }
                }
            }
            finally
            {
                await settingsStream;
                await encoderStream;
                await decoderStream;
                foreach (var stream in _streams.Values)
                {
                    stream.Abort(new ConnectionAbortedException(""));
                }
            }
        }
Ejemplo n.º 5
0
        internal async Task InnerProcessRequestsAsync <TContext>(IHttpApplication <TContext> application)
        {
            // Start other three unidirectional streams here.
            var controlTask = CreateControlStream(application);
            var encoderTask = CreateEncoderStream(application);
            var decoderTask = CreateDecoderStream(application);

            try
            {
                while (true)
                {
                    var streamContext = await _multiplexedContext.AcceptAsync();

                    if (streamContext == null || _haveSentGoAway)
                    {
                        break;
                    }

                    var quicStreamFeature = streamContext.Features.Get <IStreamDirectionFeature>();
                    var streamIdFeature   = streamContext.Features.Get <IStreamIdFeature>();

                    Debug.Assert(quicStreamFeature != null);

                    var httpConnectionContext = new Http3StreamContext
                    {
                        ConnectionId  = streamContext.ConnectionId,
                        StreamContext = streamContext,
                        // TODO connection context is null here. Should we set it to anything?
                        ServiceContext     = _context.ServiceContext,
                        ConnectionFeatures = streamContext.Features,
                        MemoryPool         = _context.MemoryPool,
                        Transport          = streamContext.Transport,
                        TimeoutControl     = _context.TimeoutControl,
                        LocalEndPoint      = streamContext.LocalEndPoint as IPEndPoint,
                        RemoteEndPoint     = streamContext.RemoteEndPoint as IPEndPoint
                    };

                    if (!quicStreamFeature.CanWrite)
                    {
                        // Unidirectional stream
                        var stream = new Http3ControlStream <TContext>(application, this, httpConnectionContext);
                        ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                    }
                    else
                    {
                        // Keep track of highest stream id seen for GOAWAY
                        var streamId = streamIdFeature.StreamId;
                        HighestStreamId = streamId;

                        var http3Stream = new Http3Stream <TContext>(application, this, httpConnectionContext);
                        var stream      = http3Stream;
                        lock (_streams)
                        {
                            _streams[streamId] = http3Stream;
                        }
                        KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
                        ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                    }
                }
            }
            finally
            {
                // Abort all streams as connection has shutdown.
                lock (_streams)
                {
                    foreach (var stream in _streams.Values)
                    {
                        stream.Abort(new ConnectionAbortedException("Connection is shutting down."));
                    }
                }

                ControlStream?.Abort(new ConnectionAbortedException("Connection is shutting down."));
                EncoderStream?.Abort(new ConnectionAbortedException("Connection is shutting down."));
                DecoderStream?.Abort(new ConnectionAbortedException("Connection is shutting down."));

                await controlTask;
                await encoderTask;
                await decoderTask;
            }
        }
Ejemplo n.º 6
0
        internal async Task InnerProcessStreamsAsync <TContext>(IHttpApplication <TContext> application) where TContext : notnull
        {
            // An endpoint MAY avoid creating an encoder stream if it's not going to
            // be used(for example if its encoder doesn't wish to use the dynamic
            // table, or if the maximum size of the dynamic table permitted by the
            // peer is zero).

            // An endpoint MAY avoid creating a decoder stream if its decoder sets
            // the maximum capacity of the dynamic table to zero.

            // Don't create Encoder and Decoder as they aren't used now.
            Exception?error = null;

            // TODO should we await the control stream task?
            var controlTask = CreateControlStream(application);

            _timeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);

            try
            {
                while (_isClosed == 0)
                {
                    // TODO implement way to unblock loop for one call to accept async to update state.
                    // Use cts for now, update to custom awaitable or different solution in the future.
                    var streamContext = await _multiplexedContext.AcceptAsync();

                    try
                    {
                        if (streamContext == null)
                        {
                            break;
                        }

                        var quicStreamFeature = streamContext.Features.Get <IStreamDirectionFeature>();
                        var streamIdFeature   = streamContext.Features.Get <IStreamIdFeature>();

                        Debug.Assert(quicStreamFeature != null);
                        Debug.Assert(streamIdFeature != null);

                        var httpConnectionContext = new Http3StreamContext(
                            streamContext.ConnectionId,
                            protocols: default,
                            connectionContext: null !, // TODO connection context is null here. Should we set it to anything?
                            _context.ServiceContext,
                            streamContext.Features,
                            _context.MemoryPool,
                            streamContext.LocalEndPoint as IPEndPoint,
                            streamContext.RemoteEndPoint as IPEndPoint,
                            streamContext.Transport,
                            streamContext,
                            _serverSettings);
                        httpConnectionContext.TimeoutControl = _context.TimeoutControl;

                        if (!quicStreamFeature.CanWrite)
                        {
                            // Unidirectional stream
                            var stream = new Http3ControlStream <TContext>(application, this, httpConnectionContext);
                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                        else
                        {
                            var streamId = streamIdFeature.StreamId;

                            HighestStreamId = streamId;

                            var http3Stream = new Http3Stream <TContext>(application, this, httpConnectionContext);
                            var stream      = http3Stream;
                            lock (_streams)
                            {
                                _activeRequestCount++;
                                _streams[streamId] = http3Stream;
                            }
                            KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                    }
Ejemplo n.º 7
0
        public async Task ProcessRequestsAsync <TContext>(IHttpApplication <TContext> application) where TContext : notnull
        {
            // An endpoint MAY avoid creating an encoder stream if it's not going to
            // be used(for example if its encoder doesn't wish to use the dynamic
            // table, or if the maximum size of the dynamic table permitted by the
            // peer is zero).

            // An endpoint MAY avoid creating a decoder stream if its decoder sets
            // the maximum capacity of the dynamic table to zero.

            // Don't create Encoder and Decoder as they aren't used now.

            Exception?error = null;
            ValueTask outboundControlStreamTask = default;

            try
            {
                var outboundControlStream = await CreateNewUnidirectionalStreamAsync(application);

                lock (_sync)
                {
                    OutboundControlStream = outboundControlStream;
                }

                // Don't delay on waiting to send outbound control stream settings.
                outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream);

                while (_isClosed == 0)
                {
                    // Don't pass a cancellation token to AcceptAsync.
                    // AcceptAsync will return null if the connection is gracefully shutting down or aborted.
                    var streamContext = await _multiplexedContext.AcceptAsync();

                    try
                    {
                        if (streamContext == null)
                        {
                            break;
                        }

                        var streamDirectionFeature = streamContext.Features.Get <IStreamDirectionFeature>();
                        var streamIdFeature        = streamContext.Features.Get <IStreamIdFeature>();

                        Debug.Assert(streamDirectionFeature != null);
                        Debug.Assert(streamIdFeature != null);

                        if (!streamDirectionFeature.CanWrite)
                        {
                            // Unidirectional stream
                            var stream = new Http3ControlStream <TContext>(application, CreateHttpStreamContext(streamContext));
                            _streamLifetimeHandler.OnStreamCreated(stream);

                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                        else
                        {
                            var persistentStateFeature = streamContext.Features.Get <IPersistentStateFeature>();
                            Debug.Assert(persistentStateFeature != null, $"Required {nameof(IPersistentStateFeature)} not on stream context.");

                            // Request stream
                            UpdateHighestStreamId(streamIdFeature.StreamId);

                            Http3Stream <TContext> stream;

                            // Check whether there is an existing HTTP/3 stream on the transport stream.
                            // A stream will only be cached if the transport stream itself is reused.
                            if (!persistentStateFeature.State.TryGetValue(StreamPersistentStateKey, out var s))
                            {
                                stream = new Http3Stream <TContext>(application, CreateHttpStreamContext(streamContext));
                                persistentStateFeature.State.Add(StreamPersistentStateKey, stream);
                            }
                            else
                            {
                                stream = (Http3Stream <TContext>)s !;
                                stream.InitializeWithExistingContext(streamContext.Transport);
                            }

                            _streamLifetimeHandler.OnStreamCreated(stream);

                            KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                    }
                    finally
                    {
                        UpdateConnectionState();
                    }
                }
            }
            catch (ConnectionResetException ex)
            {
                lock (_streams)
                {
                    if (_activeRequestCount > 0)
                    {
                        Log.RequestProcessingError(_context.ConnectionId, ex);
                    }
                }
                error = ex;
            }
            catch (IOException ex)
            {
                Log.RequestProcessingError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (ConnectionAbortedException ex)
            {
                Log.RequestProcessingError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (Http3ConnectionErrorException ex)
            {
                Log.Http3ConnectionError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (Exception ex)
            {
                error = ex;
            }
            finally
            {
                var connectionError = error as ConnectionAbortedException
                                      ?? new ConnectionAbortedException(CoreStrings.Http3ConnectionFaulted, error !);

                try
                {
                    if (TryClose())
                    {
                        // This throws when connection is shut down.
                        // TODO how to make it so we can distinguish between Abort from server vs client?
                        await SendGoAway(GetHighestStreamId());
                    }

                    foreach (var stream in _streams.Values)
                    {
                        stream.Abort(connectionError, (Http3ErrorCode)_errorCodeFeature.Error);
                    }

                    lock (_sync)
                    {
                        OutboundControlStream?.Abort(connectionError, (Http3ErrorCode)_errorCodeFeature.Error);
                    }

                    while (_activeRequestCount > 0)
                    {
                        await _streamCompletionAwaitable;
                    }

                    await outboundControlStreamTask;

                    _context.TimeoutControl.CancelTimeout();
                    _context.TimeoutControl.StartDrainTimeout(Limits.MinResponseDataRate, Limits.MaxResponseBufferSize);
                }
                catch
                {
                    Abort(connectionError, Http3ErrorCode.InternalError);
                    throw;
                }
            }
        }
Ejemplo n.º 8
0
        public async Task ProcessRequestsAsync <TContext>(IHttpApplication <TContext> application) where TContext : notnull
        {
            // An endpoint MAY avoid creating an encoder stream if it's not going to
            // be used(for example if its encoder doesn't wish to use the dynamic
            // table, or if the maximum size of the dynamic table permitted by the
            // peer is zero).

            // An endpoint MAY avoid creating a decoder stream if its decoder sets
            // the maximum capacity of the dynamic table to zero.

            // Don't create Encoder and Decoder as they aren't used now.

            Exception?error = null;
            ValueTask outboundControlStreamTask = default;

            try
            {
                var outboundControlStream = await CreateNewUnidirectionalStreamAsync(application);

                lock (_sync)
                {
                    OutboundControlStream = outboundControlStream;
                }

                // Don't delay on waiting to send outbound control stream settings.
                outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream);

                while (_isClosed == 0)
                {
                    // Don't pass a cancellation token to AcceptAsync.
                    // AcceptAsync will return null if the connection is gracefully shutting down or aborted.
                    var streamContext = await _multiplexedContext.AcceptAsync();

                    try
                    {
                        if (streamContext == null)
                        {
                            break;
                        }

                        var streamDirectionFeature = streamContext.Features.Get <IStreamDirectionFeature>();
                        var streamIdFeature        = streamContext.Features.Get <IStreamIdFeature>();

                        Debug.Assert(streamDirectionFeature != null);
                        Debug.Assert(streamIdFeature != null);

                        var httpConnectionContext = new Http3StreamContext(
                            streamContext.ConnectionId,
                            protocols: default,
                            connectionContext: null !, // TODO connection context is null here. Should we set it to anything?
                            _context.ServiceContext,
                            streamContext.Features,
                            _context.MemoryPool,
                            streamContext.LocalEndPoint as IPEndPoint,
                            streamContext.RemoteEndPoint as IPEndPoint,
                            streamContext.Transport,
                            _streamLifetimeHandler,
                            streamContext,
                            _clientSettings,
                            _serverSettings);
                        httpConnectionContext.TimeoutControl = _context.TimeoutControl;

                        if (!streamDirectionFeature.CanWrite)
                        {
                            // Unidirectional stream
                            var stream = new Http3ControlStream <TContext>(application, httpConnectionContext);
                            _streamLifetimeHandler.OnStreamCreated(stream);

                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                        else
                        {
                            // Request stream
                            UpdateHighestStreamId(streamIdFeature.StreamId);

                            var stream = new Http3Stream <TContext>(application, httpConnectionContext);
                            _streamLifetimeHandler.OnStreamCreated(stream);

                            KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                    }
Ejemplo n.º 9
0
        public async Task ProcessRequestsAsync <TContext>(IHttpApplication <TContext> application)
        {
            var streamListenerFeature = Context.ConnectionFeatures.Get <IQuicStreamListenerFeature>();

            // Start other three unidirectional streams here.
            var controlTask = CreateControlStream(application);
            var encoderTask = CreateEncoderStream(application);
            var decoderTask = CreateDecoderStream(application);

            try
            {
                while (true)
                {
                    var connectionContext = await streamListenerFeature.AcceptAsync();

                    if (connectionContext == null || _haveSentGoAway)
                    {
                        break;
                    }

                    var httpConnectionContext = new HttpConnectionContext
                    {
                        ConnectionId       = connectionContext.ConnectionId,
                        ConnectionContext  = connectionContext,
                        Protocols          = Context.Protocols,
                        ServiceContext     = Context.ServiceContext,
                        ConnectionFeatures = connectionContext.Features,
                        MemoryPool         = Context.MemoryPool,
                        Transport          = connectionContext.Transport,
                        TimeoutControl     = Context.TimeoutControl,
                        LocalEndPoint      = connectionContext.LocalEndPoint as IPEndPoint,
                        RemoteEndPoint     = connectionContext.RemoteEndPoint as IPEndPoint
                    };

                    var streamFeature = httpConnectionContext.ConnectionFeatures.Get <IQuicStreamFeature>();

                    if (!streamFeature.CanWrite)
                    {
                        // Unidirectional stream
                        var stream = new Http3ControlStream <TContext>(application, this, httpConnectionContext);
                        ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                    }
                    else
                    {
                        // Keep track of highest stream id seen for GOAWAY
                        var streamId = streamFeature.StreamId;

                        HighestStreamId = streamId;

                        var http3Stream = new Http3Stream <TContext>(application, this, httpConnectionContext);
                        var stream      = http3Stream;
                        _streams[streamId] = http3Stream;
                        ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                    }
                }
            }
            finally
            {
                // Abort all streams as connection has shutdown.
                foreach (var stream in _streams.Values)
                {
                    stream.Abort(new ConnectionAbortedException("Connection is shutting down."));
                }

                ControlStream.Abort(new ConnectionAbortedException("Connection is shutting down."));
                EncoderStream.Abort(new ConnectionAbortedException("Connection is shutting down."));
                DecoderStream.Abort(new ConnectionAbortedException("Connection is shutting down."));

                await controlTask;
                await encoderTask;
                await decoderTask;
            }
        }