diff --git a/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.Log.cs b/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.Log.cs index cf983c804b..799c43af43 100644 --- a/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.Log.cs +++ b/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.Log.cs @@ -49,6 +49,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal private static readonly Action _connectionDisposedWhileWriteInProgress = LoggerMessage.Define(LogLevel.Debug, new EventId(13, "ConnectionDisposedWhileWriteInProgress"), "Connection {TransportConnectionId} was disposed while a write was in progress."); + private static readonly Action _failedToReadHttpRequestBody = + LoggerMessage.Define(LogLevel.Debug, new EventId(14, "FailedToReadHttpRequestBody"), "Connection {TransportConnectionId} failed to read the HTTP request body."); + public static void ConnectionDisposed(ILogger logger, string connectionId) { _connectionDisposed(logger, connectionId, null); @@ -113,6 +116,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal { _connectionDisposedWhileWriteInProgress(logger, connectionId, ex); } + + public static void FailedToReadHttpRequestBody(ILogger logger, string connectionId, Exception ex) + { + _failedToReadHttpRequestBody(logger, connectionId, ex); + } } } } diff --git a/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.cs b/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.cs index 4d4bd93c1d..3d36933d3c 100644 --- a/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.cs +++ b/src/Microsoft.AspNetCore.Http.Connections/Internal/HttpConnectionDispatcher.cs @@ -505,6 +505,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal context.Response.ContentType = "text/plain"; return; } + catch (IOException ex) + { + // Can occur when the HTTP request is canceled by the client + Log.FailedToReadHttpRequestBody(_logger, connection.ConnectionId, ex); + + context.Response.StatusCode = StatusCodes.Status400BadRequest; + context.Response.ContentType = "text/plain"; + return; + } Log.ReceivedBytes(_logger, connection.ApplicationStream.Length); } diff --git a/test/Microsoft.AspNetCore.Http.Connections.Tests/HttpConnectionDispatcherTests.cs b/test/Microsoft.AspNetCore.Http.Connections.Tests/HttpConnectionDispatcherTests.cs index 39d19723bd..0761fe7c2b 100644 --- a/test/Microsoft.AspNetCore.Http.Connections.Tests/HttpConnectionDispatcherTests.cs +++ b/test/Microsoft.AspNetCore.Http.Connections.Tests/HttpConnectionDispatcherTests.cs @@ -25,6 +25,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; +using Moq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; @@ -719,6 +720,44 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests } } + [Theory] + [InlineData(HttpTransportType.LongPolling)] + [InlineData(HttpTransportType.ServerSentEvents)] + public async Task IOExceptionWhenReadingRequestReturns400Response(HttpTransportType transportType) + { + using (StartVerifiableLog(out var loggerFactory, LogLevel.Debug)) + { + var manager = CreateConnectionManager(loggerFactory); + var dispatcher = new HttpConnectionDispatcher(manager, loggerFactory); + var connection = manager.CreateConnection(); + connection.TransportType = transportType; + + var mockStream = new Mock(); + mockStream.Setup(m => m.CopyToAsync(It.IsAny(), It.IsAny(), It.IsAny())).Throws(new IOException()); + + using (var responseBody = new MemoryStream()) + { + var context = new DefaultHttpContext(); + context.Request.Body = mockStream.Object; + context.Response.Body = responseBody; + + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddOptions(); + context.Request.Path = "/foo"; + context.Request.Method = "POST"; + var values = new Dictionary(); + values["id"] = connection.ConnectionId; + var qs = new QueryCollection(values); + context.Request.Query = qs; + + await dispatcher.ExecuteAsync(context, new HttpConnectionDispatcherOptions(), c => Task.CompletedTask); + + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + } + } + } + [Fact] public async Task EndpointsThatRequireConnectionId400WhenNoConnectionIdProvidedForPost() {