Пример #1
0
        public async Task ExecuteAsync(HttpContext context, HttpConnectionOptions options, ConnectionDelegate connectionDelegate)
        {
            // Create the log scope and attempt to pass the Connection ID to it so as many logs as possible contain
            // the Connection ID metadata. If this is the negotiate request then the Connection ID for the scope will
            // be set a little later.
            var logScope = new ConnectionLogScope(GetConnectionId(context));

            using (_logger.BeginScope(logScope))
            {
                if (!await AuthorizeHelper.AuthorizeAsync(context, options.AuthorizationData))
                {
                    return;
                }

                if (HttpMethods.IsPost(context.Request.Method))
                {
                    // POST /{path}
                    await ProcessSend(context, options);
                }
                else if (HttpMethods.IsGet(context.Request.Method))
                {
                    // GET /{path}
                    await ExecuteAsync(context, connectionDelegate, options, logScope);
                }
                else
                {
                    context.Response.ContentType = "text/plain";
                    context.Response.StatusCode  = StatusCodes.Status405MethodNotAllowed;
                }
            }
        }
Пример #2
0
 public HttpConnection(Uri url, ITransportFactory transportFactory, ILoggerFactory loggerFactory, HttpOptions httpOptions)
 {
     Url               = url ?? throw new ArgumentNullException(nameof(url));
     _loggerFactory    = loggerFactory ?? NullLoggerFactory.Instance;
     _logger           = _loggerFactory.CreateLogger <HttpConnection>();
     _httpOptions      = httpOptions;
     _httpClient       = CreateHttpClient();
     _transportFactory = transportFactory ?? throw new ArgumentNullException(nameof(transportFactory));
     _logScope         = new ConnectionLogScope();
     _scopeDisposable  = _logger.BeginScope(_logScope);
 }
Пример #3
0
        public HttpConnection(Uri url, TransportType transportType, ILoggerFactory loggerFactory, HttpOptions httpOptions)
        {
            Url = url ?? throw new ArgumentNullException(nameof(url));

            _loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
            _logger        = _loggerFactory.CreateLogger <HttpConnection>();
            _httpOptions   = httpOptions;

            _requestedTransportType = transportType;
            if (_requestedTransportType != TransportType.WebSockets)
            {
                _httpClient = CreateHttpClient();
            }

            _transportFactory = new DefaultTransportFactory(transportType, _loggerFactory, _httpClient, httpOptions);
            _logScope         = new ConnectionLogScope();
            _scopeDisposable  = _logger.BeginScope(_logScope);
        }
Пример #4
0
    public async Task ExecuteNegotiateAsync(HttpContext context, HttpConnectionDispatcherOptions options)
    {
        // Create the log scope and the scope connectionId param will be set when the connection is created.
        var logScope = new ConnectionLogScope(connectionId: string.Empty);

        using (_logger.BeginScope(logScope))
        {
            if (HttpMethods.IsPost(context.Request.Method))
            {
                // POST /{path}/negotiate
                await ProcessNegotiate(context, options, logScope);
            }
            else
            {
                context.Response.ContentType = "text/plain";
                context.Response.StatusCode  = StatusCodes.Status405MethodNotAllowed;
            }
        }
    }
Пример #5
0
    public async Task ExecuteAsync(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionDelegate connectionDelegate)
    {
        // Create the log scope and attempt to pass the Connection ID to it so as many logs as possible contain
        // the Connection ID metadata. If this is the negotiate request then the Connection ID for the scope will
        // be set a little later.

        HttpConnectionContext?connectionContext = null;
        var connectionToken = GetConnectionToken(context);

        if (!StringValues.IsNullOrEmpty(connectionToken))
        {
            // Use ToString; IsNullOrEmpty doesn't tell the compiler anything about implicit conversion to string.
            _manager.TryGetConnection(connectionToken.ToString(), out connectionContext);
        }

        var logScope = new ConnectionLogScope(connectionContext?.ConnectionId);

        using (_logger.BeginScope(logScope))
        {
            if (HttpMethods.IsPost(context.Request.Method))
            {
                // POST /{path}
                await ProcessSend(context, options);
            }
            else if (HttpMethods.IsGet(context.Request.Method))
            {
                // GET /{path}
                await ExecuteAsync(context, connectionDelegate, options, logScope);
            }
            else if (HttpMethods.IsDelete(context.Request.Method))
            {
                // DELETE /{path}
                await ProcessDeleteAsync(context);
            }
            else
            {
                context.Response.ContentType = "text/plain";
                context.Response.StatusCode  = StatusCodes.Status405MethodNotAllowed;
            }
        }
    }
        public async Task ExecuteNegotiateAsync(HttpContext context, HttpSocketOptions options)
        {
            // Create the log scope and the scope connectionId param will be set when the connection is created.
            var logScope = new ConnectionLogScope(connectionId: string.Empty);

            using (_logger.BeginScope(logScope))
            {
                if (!await AuthorizeHelper.AuthorizeAsync(context, options.AuthorizationData))
                {
                    return;
                }

                if (HttpMethods.IsPost(context.Request.Method))
                {
                    // POST /{path}/negotiate
                    await ProcessNegotiate(context, options, logScope);
                }
                else
                {
                    context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
                }
            }
        }
Пример #7
0
        private async Task <bool> EnsureConnectionStateAsync(HttpConnectionContext connection, HttpContext context, HttpTransportType transportType, HttpTransportType supportedTransports, ConnectionLogScope logScope, HttpConnectionOptions options)
        {
            if ((supportedTransports & transportType) == 0)
            {
                context.Response.ContentType = "text/plain";
                context.Response.StatusCode  = StatusCodes.Status404NotFound;
                Log.TransportNotSupported(_logger, transportType);
                await context.Response.WriteAsync($"{transportType} transport not supported by this end point type");

                return(false);
            }

            // Set the IHttpConnectionFeature now that we can access it.
            connection.Features.Set(context.Features.Get <IHttpConnectionFeature>());

            if (connection.TransportType == HttpTransportType.None)
            {
                connection.TransportType = transportType;
                connection.Items[ConnectionMetadataNames.Transport] = transportType;
            }
            else if (connection.TransportType != transportType)
            {
                context.Response.ContentType = "text/plain";
                context.Response.StatusCode  = StatusCodes.Status400BadRequest;
                Log.CannotChangeTransport(_logger, connection.TransportType, transportType);
                await context.Response.WriteAsync("Cannot change transports mid-connection");

                return(false);
            }

            // Setup the connection state from the http context
            connection.User = context.User;

            // Configure transport-specific features.
            if (transportType == HttpTransportType.LongPolling)
            {
                connection.Features.Set <IConnectionInherentKeepAliveFeature>(new ConnectionInherentKeepAliveFeature(options.LongPolling.PollTimeout));

                // For long polling, the requests come and go but the connection is still alive.
                // To make the IHttpContextFeature work well, we make a copy of the relevant properties
                // to a new HttpContext. This means that it's impossible to affect the context
                // with subsequent requests.
                var existing = connection.HttpContext;
                if (existing == null)
                {
                    var httpContext = CloneHttpContext(context);
                    connection.HttpContext = httpContext;
                }
                else
                {
                    // Set the request trace identifier to the current http request handling the poll
                    existing.TraceIdentifier = context.TraceIdentifier;
                    existing.User            = context.User;
                }
            }
            else
            {
                connection.HttpContext = context;
            }

            // Set the Connection ID on the logging scope so that logs from now on will have the
            // Connection ID metadata set.
            logScope.ConnectionId = connection.ConnectionId;

            return(true);
        }
Пример #8
0
        private async Task ProcessNegotiate(HttpContext context, HttpConnectionOptions options, ConnectionLogScope logScope)
        {
            context.Response.ContentType = "application/json";

            // Establish the connection
            var connection = CreateConnection(options);

            // Set the Connection ID on the logging scope so that logs from now on will have the
            // Connection ID metadata set.
            logScope.ConnectionId = connection.ConnectionId;

            // Don't use thread static instance here because writer is used with async
            var writer = new MemoryBufferWriter();

            try
            {
                // Get the bytes for the connection id
                WriteNegotiatePayload(writer, connection.ConnectionId, context, options);

                Log.NegotiationRequest(_logger);

                // Write it out to the response with the right content length
                context.Response.ContentLength = writer.Length;
                await writer.CopyToAsync(context.Response.Body);
            }
            finally
            {
                writer.Reset();
            }
        }
Пример #9
0
        private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connectionDelegate, HttpConnectionOptions options, ConnectionLogScope logScope)
        {
            var supportedTransports = options.Transports;

            // Server sent events transport
            // GET /{path}
            // Accept: text/event-stream
            var headers = context.Request.GetTypedHeaders();

            if (headers.Accept?.Contains(new Net.Http.Headers.MediaTypeHeaderValue("text/event-stream")) == true)
            {
                // Connection must already exist
                var connection = await GetConnectionAsync(context, options);

                if (connection == null)
                {
                    // No such connection, GetConnection already set the response status code
                    return;
                }

                if (!await EnsureConnectionStateAsync(connection, context, HttpTransportType.ServerSentEvents, supportedTransports, logScope, options))
                {
                    // Bad connection state. It's already set the response status code.
                    return;
                }

                Log.EstablishedConnection(_logger);

                // ServerSentEvents is a text protocol only
                connection.SupportedFormats = TransferFormat.Text;

                // We only need to provide the Input channel since writing to the application is handled through /send.
                var sse = new ServerSentEventsTransport(connection.Application.Input, connection.ConnectionId, _loggerFactory);

                await DoPersistentConnection(connectionDelegate, sse, context, connection);
            }
            else if (context.WebSockets.IsWebSocketRequest)
            {
                // Connection can be established lazily
                var connection = await GetOrCreateConnectionAsync(context, options);

                if (connection == null)
                {
                    // No such connection, GetOrCreateConnection already set the response status code
                    return;
                }

                if (!await EnsureConnectionStateAsync(connection, context, HttpTransportType.WebSockets, supportedTransports, logScope, options))
                {
                    // Bad connection state. It's already set the response status code.
                    return;
                }

                Log.EstablishedConnection(_logger);

                var ws = new WebSocketsTransport(options.WebSockets, connection.Application, connection, _loggerFactory);

                await DoPersistentConnection(connectionDelegate, ws, context, connection);
            }
            else
            {
                // GET /{path} maps to long polling

                // Connection must already exist
                var connection = await GetConnectionAsync(context, options);

                if (connection == null)
                {
                    // No such connection, GetConnection already set the response status code
                    return;
                }

                if (!await EnsureConnectionStateAsync(connection, context, HttpTransportType.LongPolling, supportedTransports, logScope, options))
                {
                    // Bad connection state. It's already set the response status code.
                    return;
                }

                try
                {
                    await connection.Lock.WaitAsync();

                    if (connection.Status == HttpConnectionContext.ConnectionStatus.Disposed)
                    {
                        Log.ConnectionDisposed(_logger, connection.ConnectionId);

                        // The connection was disposed
                        context.Response.StatusCode  = StatusCodes.Status404NotFound;
                        context.Response.ContentType = "text/plain";
                        return;
                    }

                    if (connection.Status == HttpConnectionContext.ConnectionStatus.Active)
                    {
                        var existing = connection.GetHttpContext();
                        Log.ConnectionAlreadyActive(_logger, connection.ConnectionId, existing.TraceIdentifier);

                        using (connection.Cancellation)
                        {
                            // Cancel the previous request
                            connection.Cancellation.Cancel();

                            // Wait for the previous request to drain
                            await connection.TransportTask;

                            Log.PollCanceled(_logger, connection.ConnectionId, existing.TraceIdentifier);
                        }
                    }

                    // Mark the connection as active
                    connection.Status = HttpConnectionContext.ConnectionStatus.Active;

                    // Raise OnConnected for new connections only since polls happen all the time
                    if (connection.ApplicationTask == null)
                    {
                        Log.EstablishedConnection(_logger);

                        connection.ApplicationTask = ExecuteApplication(connectionDelegate, connection);
                    }
                    else
                    {
                        Log.ResumingConnection(_logger);
                    }

                    // REVIEW: Performance of this isn't great as this does a bunch of per request allocations
                    connection.Cancellation = new CancellationTokenSource();

                    var timeoutSource = new CancellationTokenSource();
                    var tokenSource   = CancellationTokenSource.CreateLinkedTokenSource(connection.Cancellation.Token, context.RequestAborted, timeoutSource.Token);

                    // Dispose these tokens when the request is over
                    context.Response.RegisterForDispose(timeoutSource);
                    context.Response.RegisterForDispose(tokenSource);

                    var longPolling = new LongPollingTransport(timeoutSource.Token, connection.Application.Input, connection.ConnectionId, _loggerFactory);

                    // Start the transport
                    connection.TransportTask = longPolling.ProcessRequestAsync(context, tokenSource.Token);

                    // Start the timeout after we return from creating the transport task
                    timeoutSource.CancelAfter(options.LongPolling.PollTimeout);
                }
                finally
                {
                    connection.Lock.Release();
                }

                var resultTask = await Task.WhenAny(connection.ApplicationTask, connection.TransportTask);

                var pollAgain = true;

                // If the application ended before the transport task then we potentially need to end the connection
                if (resultTask == connection.ApplicationTask)
                {
                    // Complete the transport (notifying it of the application error if there is one)
                    connection.Transport.Output.Complete(connection.ApplicationTask.Exception);

                    // Wait for the transport to run
                    await connection.TransportTask;

                    // If the status code is a 204 it means the connection is done
                    if (context.Response.StatusCode == StatusCodes.Status204NoContent)
                    {
                        // We should be able to safely dispose because there's no more data being written
                        // We don't need to wait for close here since we've already waited for both sides
                        await _manager.DisposeAndRemoveAsync(connection, closeGracefully : false);

                        // Don't poll again if we've removed the connection completely
                        pollAgain = false;
                    }
                }
                else if (context.Response.StatusCode == StatusCodes.Status204NoContent)
                {
                    // Don't poll if the transport task was cancelled
                    pollAgain = false;
                }

                if (pollAgain)
                {
                    // Otherwise, we update the state to inactive again and wait for the next poll
                    try
                    {
                        await connection.Lock.WaitAsync();

                        if (connection.Status == HttpConnectionContext.ConnectionStatus.Active)
                        {
                            // Mark the connection as inactive
                            connection.LastSeenUtc = DateTime.UtcNow;

                            connection.Status = HttpConnectionContext.ConnectionStatus.Inactive;

                            // Dispose the cancellation token
                            connection.Cancellation.Dispose();

                            connection.Cancellation = null;
                        }
                    }
                    finally
                    {
                        connection.Lock.Release();
                    }
                }
            }
        }
Пример #10
0
        private async Task <bool> EnsureConnectionStateAsync(DefaultConnectionContext connection, HttpContext context, TransportType transportType, TransportType supportedTransports, ConnectionLogScope logScope, HttpSocketOptions options)
        {
            if ((supportedTransports & transportType) == 0)
            {
                context.Response.ContentType = "text/plain";
                context.Response.StatusCode  = StatusCodes.Status404NotFound;
                Log.TransportNotSupported(_logger, transportType);
                await context.Response.WriteAsync($"{transportType} transport not supported by this end point type");

                return(false);
            }

            // Set the IHttpConnectionFeature now that we can access it.
            connection.Features.Set(context.Features.Get <IHttpConnectionFeature>());

            var transport = (TransportType?)connection.Items[ConnectionMetadataNames.Transport];

            if (transport == null)
            {
                connection.Items[ConnectionMetadataNames.Transport] = transportType;
            }
            else if (transport != transportType)
            {
                context.Response.ContentType = "text/plain";
                context.Response.StatusCode  = StatusCodes.Status400BadRequest;
                Log.CannotChangeTransport(_logger, transport.Value, transportType);
                await context.Response.WriteAsync("Cannot change transports mid-connection");

                return(false);
            }

            // Configure transport-specific features.
            if (transportType == TransportType.LongPolling)
            {
                connection.Features.Set <IConnectionInherentKeepAliveFeature>(new ConnectionInherentKeepAliveFeature(options.LongPolling.PollTimeout));
            }

            // Setup the connection state from the http context
            connection.User = context.User;
            connection.SetHttpContext(context);

            // Set the Connection ID on the logging scope so that logs from now on will have the
            // Connection ID metadata set.
            logScope.ConnectionId = connection.ConnectionId;

            return(true);
        }
Пример #11
0
        private Task ProcessNegotiate(HttpContext context, HttpSocketOptions options, ConnectionLogScope logScope)
        {
            context.Response.ContentType = "application/json";

            // Establish the connection
            var connection = CreateConnectionInternal(options);

            // Set the Connection ID on the logging scope so that logs from now on will have the
            // Connection ID metadata set.
            logScope.ConnectionId = connection.ConnectionId;

            // Get the bytes for the connection id
            var negotiateResponseBuffer = Encoding.UTF8.GetBytes(GetNegotiatePayload(connection.ConnectionId, options));

            Log.NegotiationRequest(_logger);

            // Write it out to the response with the right content length
            context.Response.ContentLength = negotiateResponseBuffer.Length;
            return(context.Response.Body.WriteAsync(negotiateResponseBuffer, 0, negotiateResponseBuffer.Length));
        }
        private async Task <bool> EnsureConnectionStateAsync(DefaultConnectionContext connection, HttpContext context, TransportType transportType, TransportType supportedTransports, ConnectionLogScope logScope, HttpSocketOptions options)
        {
            if ((supportedTransports & transportType) == 0)
            {
                context.Response.ContentType = "text/plain";
                context.Response.StatusCode  = StatusCodes.Status404NotFound;
                _logger.TransportNotSupported(transportType);
                await context.Response.WriteAsync($"{transportType} transport not supported by this end point type");

                return(false);
            }

            var transport = (TransportType?)connection.Metadata[ConnectionMetadataNames.Transport];

            if (transport == null)
            {
                connection.Metadata[ConnectionMetadataNames.Transport] = transportType;
            }
            else if (transport != transportType)
            {
                context.Response.ContentType = "text/plain";
                context.Response.StatusCode  = StatusCodes.Status400BadRequest;
                _logger.CannotChangeTransport(transport.Value, transportType);
                await context.Response.WriteAsync("Cannot change transports mid-connection");

                return(false);
            }

            // Configure transport-specific features.
            if (transportType == TransportType.LongPolling)
            {
                connection.Features.Set <IConnectionInherentKeepAliveFeature>(new ConnectionInherentKeepAliveFeature(options.LongPolling.PollTimeout));
            }

            // Setup the connection state from the http context
            connection.User = context.User;
            connection.SetHttpContext(context);

            // this is the default setting which should be overwritten by transports that have different capabilities (e.g. SSE)
            connection.TransportCapabilities = TransferMode.Binary | TransferMode.Text;

            // Set the Connection ID on the logging scope so that logs from now on will have the
            // Connection ID metadata set.
            logScope.ConnectionId = connection.ConnectionId;

            return(true);
        }
        private Task ProcessNegotiate(HttpContext context, HttpSocketOptions options, ConnectionLogScope logScope)
        {
            // Set the allowed headers for this resource
            context.Response.Headers.AppendCommaSeparatedValues("Allow", "GET", "POST", "OPTIONS");

            context.Response.ContentType = "application/json";

            // Establish the connection
            var connection = _manager.CreateConnection();

            // Set the Connection ID on the logging scope so that logs from now on will have the
            // Connection ID metadata set.
            logScope.ConnectionId = connection.ConnectionId;

            // Get the bytes for the connection id
            var negotiateResponseBuffer = Encoding.UTF8.GetBytes(GetNegotiatePayload(connection.ConnectionId, options));

            _logger.NegotiationRequest();

            // Write it out to the response with the right content length
            context.Response.ContentLength = negotiateResponseBuffer.Length;
            return(context.Response.Body.WriteAsync(negotiateResponseBuffer, 0, negotiateResponseBuffer.Length));
        }
Пример #14
0
    private async Task <bool> EnsureConnectionStateAsync(HttpConnectionContext connection, HttpContext context, HttpTransportType transportType, HttpTransportType supportedTransports, ConnectionLogScope logScope, HttpConnectionDispatcherOptions options)
    {
        if ((supportedTransports & transportType) == 0)
        {
            context.Response.ContentType = "text/plain";
            context.Response.StatusCode  = StatusCodes.Status404NotFound;
            Log.TransportNotSupported(_logger, transportType);
            await context.Response.WriteAsync($"{transportType} transport not supported by this end point type");

            return(false);
        }

        // Set the IHttpConnectionFeature now that we can access it.
        connection.Features.Set(context.Features.Get <IHttpConnectionFeature>());

        if (connection.TransportType == HttpTransportType.None)
        {
            connection.TransportType = transportType;
        }
        else if (connection.TransportType != transportType)
        {
            context.Response.ContentType = "text/plain";
            context.Response.StatusCode  = StatusCodes.Status400BadRequest;
            Log.CannotChangeTransport(_logger, connection.TransportType, transportType);
            await context.Response.WriteAsync("Cannot change transports mid-connection");

            return(false);
        }

        // Configure transport-specific features.
        if (transportType == HttpTransportType.LongPolling)
        {
            connection.HasInherentKeepAlive = true;

            // For long polling, the requests come and go but the connection is still alive.
            // To make the IHttpContextFeature work well, we make a copy of the relevant properties
            // to a new HttpContext. This means that it's impossible to affect the context
            // with subsequent requests.
            var existing = connection.HttpContext;
            if (existing == null)
            {
                CloneHttpContext(context, connection);
            }
            else
            {
                // Set the request trace identifier to the current http request handling the poll
                existing.TraceIdentifier = context.TraceIdentifier;

                // Don't copy the identity if it's a windows identity
                // We specifically clone the identity on first poll if it's a windows identity
                // If we swapped the new User here we'd have to dispose the old identities which could race with the application
                // trying to access the identity.
                if (!(context.User.Identity is WindowsIdentity))
                {
                    existing.User = context.User;
                }
            }
        }
        else
        {
            connection.HttpContext = context;
        }

        // Setup the connection state from the http context
        connection.User = connection.HttpContext?.User;

        UpdateExpiration(connection, context);

        // Set the Connection ID on the logging scope so that logs from now on will have the
        // Connection ID metadata set.
        logScope.ConnectionId = connection.ConnectionId;

        return(true);
    }
Пример #15
0
    private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope)
    {
        context.Response.ContentType = "application/json";
        string?error = null;
        int    clientProtocolVersion = 0;

        if (context.Request.Query.TryGetValue("NegotiateVersion", out var queryStringVersion))
        {
            // Set the negotiate response to the protocol we use.
            var queryStringVersionValue = queryStringVersion.ToString();
            if (!int.TryParse(queryStringVersionValue, out clientProtocolVersion))
            {
                error = $"The client requested an invalid protocol version '{queryStringVersionValue}'";
                Log.InvalidNegotiateProtocolVersion(_logger, queryStringVersionValue);
            }
            else if (clientProtocolVersion < options.MinimumProtocolVersion)
            {
                error = $"The client requested version '{clientProtocolVersion}', but the server does not support this version.";
                Log.NegotiateProtocolVersionMismatch(_logger, clientProtocolVersion);
            }
            else if (clientProtocolVersion > _protocolVersion)
            {
                clientProtocolVersion = _protocolVersion;
            }
        }
        else if (options.MinimumProtocolVersion > 0)
        {
            // NegotiateVersion wasn't parsed meaning the client requests version 0.
            error = $"The client requested version '0', but the server does not support this version.";
            Log.NegotiateProtocolVersionMismatch(_logger, 0);
        }

        // Establish the connection
        HttpConnectionContext?connection = null;

        if (error == null)
        {
            connection = CreateConnection(options, clientProtocolVersion);
        }

        // Set the Connection ID on the logging scope so that logs from now on will have the
        // Connection ID metadata set.
        logScope.ConnectionId = connection?.ConnectionId;

        // Don't use thread static instance here because writer is used with async
        var writer = new MemoryBufferWriter();

        try
        {
            // Get the bytes for the connection id
            WriteNegotiatePayload(writer, connection?.ConnectionId, connection?.ConnectionToken, context, options, clientProtocolVersion, error);

            Log.NegotiationRequest(_logger);

            // Write it out to the response with the right content length
            context.Response.ContentLength = writer.Length;
            await writer.CopyToAsync(context.Response.Body);
        }
        finally
        {
            writer.Reset();
        }
    }
Пример #16
0
    private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connectionDelegate, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope)
    {
        // set a tag to allow Application Performance Management tools to differentiate long running requests for reporting purposes
        context.Features.Get <IHttpActivityFeature>()?.Activity.AddTag("http.long_running", "true");

        var supportedTransports = options.Transports;

        // Server sent events transport
        // GET /{path}
        // Accept: text/event-stream
        var headers = context.Request.GetTypedHeaders();

        if (headers.Accept?.Contains(new Net.Http.Headers.MediaTypeHeaderValue("text/event-stream")) == true)
        {
            // Connection must already exist
            var connection = await GetConnectionAsync(context);

            if (connection == null)
            {
                // No such connection, GetConnection already set the response status code
                return;
            }

            if (!await EnsureConnectionStateAsync(connection, context, HttpTransportType.ServerSentEvents, supportedTransports, logScope, options))
            {
                // Bad connection state. It's already set the response status code.
                return;
            }

            Log.EstablishedConnection(_logger);

            // ServerSentEvents is a text protocol only
            connection.SupportedFormats = TransferFormat.Text;

            // We only need to provide the Input channel since writing to the application is handled through /send.
            var sse = new ServerSentEventsServerTransport(connection.Application.Input, connection.ConnectionId, connection, _loggerFactory);

            await DoPersistentConnection(connectionDelegate, sse, context, connection);
        }
        else if (context.WebSockets.IsWebSocketRequest)
        {
            // Connection can be established lazily
            var connection = await GetOrCreateConnectionAsync(context, options);

            if (connection == null)
            {
                // No such connection, GetOrCreateConnection already set the response status code
                return;
            }

            if (!await EnsureConnectionStateAsync(connection, context, HttpTransportType.WebSockets, supportedTransports, logScope, options))
            {
                // Bad connection state. It's already set the response status code.
                return;
            }

            Log.EstablishedConnection(_logger);

            // Allow the reads to be canceled
            connection.Cancellation = new CancellationTokenSource();

            var ws = new WebSocketsServerTransport(options.WebSockets, connection.Application, connection, _loggerFactory);

            await DoPersistentConnection(connectionDelegate, ws, context, connection);
        }
        else
        {
            // GET /{path} maps to long polling

            AddNoCacheHeaders(context.Response);

            // Connection must already exist
            var connection = await GetConnectionAsync(context);

            if (connection == null)
            {
                // No such connection, GetConnection already set the response status code
                return;
            }

            if (!await EnsureConnectionStateAsync(connection, context, HttpTransportType.LongPolling, supportedTransports, logScope, options))
            {
                // Bad connection state. It's already set the response status code.
                return;
            }

            if (!await connection.CancelPreviousPoll(context))
            {
                // Connection closed. It's already set the response status code.
                return;
            }

            // Create a new Tcs every poll to keep track of the poll finishing, so we can properly wait on previous polls
            var currentRequestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

            if (!connection.TryActivateLongPollingConnection(
                    connectionDelegate, context, options.LongPolling.PollTimeout,
                    currentRequestTcs.Task, _loggerFactory, _logger))
            {
                return;
            }

            var resultTask = await Task.WhenAny(connection.ApplicationTask !, connection.TransportTask !);

            try
            {
                // If the application ended before the transport task then we potentially need to end the connection
                if (resultTask == connection.ApplicationTask)
                {
                    // Complete the transport (notifying it of the application error if there is one)
                    connection.Transport.Output.Complete(connection.ApplicationTask.Exception);

                    // Wait for the transport to run
                    // Ignore exceptions, it has been logged if there is one and the application has finished
                    // So there is no one to give the exception to
                    await connection.TransportTask !.NoThrow();

                    // If the status code is a 204 it means the connection is done
                    if (context.Response.StatusCode == StatusCodes.Status204NoContent)
                    {
                        // Cancel current request to release any waiting poll and let dispose acquire the lock
                        currentRequestTcs.TrySetCanceled();

                        // We should be able to safely dispose because there's no more data being written
                        // We don't need to wait for close here since we've already waited for both sides
                        await _manager.DisposeAndRemoveAsync(connection, closeGracefully : false);
                    }
                    else
                    {
                        // Only allow repoll if we aren't removing the connection.
                        connection.MarkInactive();
                    }
                }
                else if (resultTask.IsFaulted || resultTask.IsCanceled)
                {
                    // Cancel current request to release any waiting poll and let dispose acquire the lock
                    currentRequestTcs.TrySetCanceled();
                    // We should be able to safely dispose because there's no more data being written
                    // We don't need to wait for close here since we've already waited for both sides
                    await _manager.DisposeAndRemoveAsync(connection, closeGracefully : false);
                }
                else
                {
                    // Only allow repoll if we aren't removing the connection.
                    connection.MarkInactive();
                }
            }
            finally
            {
                // Artificial task queue
                // This will cause incoming polls to wait until the previous poll has finished updating internal state info
                currentRequestTcs.TrySetResult();
            }
        }
    }