Separate request rejection from bad request state setting.

This commit is contained in:
Cesar Blum Silveira 2016-10-12 10:12:01 -07:00
parent f1071dea50
commit 78584799a4
3 changed files with 134 additions and 63 deletions

View File

@ -47,7 +47,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private readonly object _onStartingSync = new Object(); private readonly object _onStartingSync = new Object();
private readonly object _onCompletedSync = new Object(); private readonly object _onCompletedSync = new Object();
protected bool _requestRejected;
private Streams _frameStreams; private Streams _frameStreams;
protected Stack<KeyValuePair<Func<object, Task>, object>> _onStarting; protected Stack<KeyValuePair<Func<object, Task>, object>> _onStarting;
@ -64,6 +63,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private bool _canHaveBody; private bool _canHaveBody;
private bool _autoChunk; private bool _autoChunk;
protected Exception _applicationException; protected Exception _applicationException;
private BadHttpRequestException _requestRejectedException;
protected HttpVersion _httpVersion; protected HttpVersion _httpVersion;
@ -717,7 +717,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
protected Task TryProduceInvalidRequestResponse() protected Task TryProduceInvalidRequestResponse()
{ {
if (_requestRejected) if (_requestRejectedException != null)
{ {
if (FrameRequestHeaders == null || FrameResponseHeaders == null) if (FrameRequestHeaders == null || FrameResponseHeaders == null)
{ {
@ -732,7 +732,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
protected Task ProduceEnd() protected Task ProduceEnd()
{ {
if (_requestRejected || _applicationException != null) if (_requestRejectedException != null || _applicationException != null)
{ {
if (HasResponseStarted) if (HasResponseStarted)
{ {
@ -741,8 +741,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
} }
// If the request was rejected, the error state has already been set by SetBadRequestState // If the request was rejected, the error state has already been set by SetBadRequestState and
if (!_requestRejected) // that should take precedence.
if (_requestRejectedException != null)
{
SetErrorResponseHeaders(statusCode: _requestRejectedException.StatusCode);
}
else
{ {
// 500 Internal Server Error // 500 Internal Server Error
SetErrorResponseHeaders(statusCode: 500); SetErrorResponseHeaders(statusCode: 500);
@ -1394,24 +1399,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_applicationException); _applicationException);
} }
public void RejectRequest(string message)
{
throw new ObjectDisposedException(
"The response has been aborted due to an unhandled application exception.",
_applicationException);
}
public void RejectRequest(RequestRejectionReason reason) public void RejectRequest(RequestRejectionReason reason)
{ {
var ex = BadHttpRequestException.GetException(reason); RejectRequest(BadHttpRequestException.GetException(reason));
SetBadRequestState(ex);
throw ex;
} }
public void RejectRequest(RequestRejectionReason reason, string value) public void RejectRequest(RequestRejectionReason reason, string value)
{ {
var ex = BadHttpRequestException.GetException(reason, value); RejectRequest(BadHttpRequestException.GetException(reason, value));
SetBadRequestState(ex); }
private void RejectRequest(BadHttpRequestException ex)
{
Log.ConnectionBadRequest(ConnectionId, ex);
throw ex; throw ex;
} }
@ -1422,17 +1422,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public void SetBadRequestState(BadHttpRequestException ex) public void SetBadRequestState(BadHttpRequestException ex)
{ {
// Setting status code will throw if response has already started
if (!HasResponseStarted) if (!HasResponseStarted)
{ {
SetErrorResponseHeaders(statusCode: ex.StatusCode); SetErrorResponseHeaders(ex.StatusCode);
} }
_keepAlive = false; _keepAlive = false;
_requestProcessingStopping = true; _requestProcessingStopping = true;
_requestRejected = true; _requestRejectedException = ex;
Log.ConnectionBadRequest(ConnectionId, ex);
} }
protected void ReportApplicationError(Exception ex) protected void ReportApplicationError(Exception ex)

View File

@ -100,6 +100,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
catch (Exception ex) catch (Exception ex)
{ {
ReportApplicationError(ex); ReportApplicationError(ex);
if (ex is BadHttpRequestException)
{
throw;
}
} }
finally finally
{ {
@ -118,30 +123,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
await FireOnCompleted(); await FireOnCompleted();
} }
// If _requestAbort is set, the connection has already been closed.
if (Volatile.Read(ref _requestAborted) == 0)
{
ResumeStreams();
if (_keepAlive)
{
// Finish reading the request body in case the app did not.
await messageBody.Consume();
}
// ProduceEnd() must be called before _application.DisposeContext(), to ensure
// HttpContext.Response.StatusCode is correctly set when
// IHttpContextFactory.Dispose(HttpContext) is called.
await ProduceEnd();
}
else if (!HasResponseStarted)
{
// If the request was aborted and no response was sent, there's no
// meaningful status code to log.
StatusCode = 0;
}
} }
// If _requestAbort is set, the connection has already been closed.
if (Volatile.Read(ref _requestAborted) == 0)
{
ResumeStreams();
if (_keepAlive)
{
// Finish reading the request body in case the app did not.
await messageBody.Consume();
}
// ProduceEnd() must be called before _application.DisposeContext(), to ensure
// HttpContext.Response.StatusCode is correctly set when
// IHttpContextFactory.Dispose(HttpContext) is called.
await ProduceEnd();
}
else if (!HasResponseStarted)
{
// If the request was aborted and no response was sent, there's no
// meaningful status code to log.
StatusCode = 0;
}
}
catch (BadHttpRequestException ex)
{
// Handle BadHttpRequestException thrown during app execution or remaining message body consumption.
// This has to be caught here so StatusCode is set properly before disposing the HttpContext
// (DisposeContext logs StatusCode).
SetBadRequestState(ex);
} }
finally finally
{ {
@ -169,11 +181,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
catch (BadHttpRequestException ex) catch (BadHttpRequestException ex)
{ {
if (!_requestRejected) // Handle BadHttpRequestException thrown during request line or header parsing.
{ // SetBadRequestState logs the error.
// SetBadRequestState logs the error. SetBadRequestState(ex);
SetBadRequestState(ex);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -183,11 +193,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
try try
{ {
await TryProduceInvalidRequestResponse();
// If _requestAborted is set, the connection has already been closed. // If _requestAborted is set, the connection has already been closed.
if (Volatile.Read(ref _requestAborted) == 0) if (Volatile.Read(ref _requestAborted) == 0)
{ {
await TryProduceInvalidRequestResponse();
ConnectionControl.End(ProduceEndType.SocketShutdown); ConnectionControl.End(ProduceEndType.SocketShutdown);
} }
} }

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@ -239,6 +238,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
sendMalformedRequest: true); sendMalformedRequest: true);
} }
[Fact]
public Task ResponseStatusCodeSetBeforeHttpContextDisposedRequestMalformedRead()
{
return ResponseStatusCodeSetBeforeHttpContextDispose(
async context =>
{
await context.Request.Body.ReadAsync(new byte[1], 0, 1);
},
expectedClientStatusCode: null,
expectedServerStatusCode: HttpStatusCode.BadRequest,
sendMalformedRequest: true);
}
[Fact]
public Task ResponseStatusCodeSetBeforeHttpContextDisposedRequestMalformedReadIgnored()
{
return ResponseStatusCodeSetBeforeHttpContextDispose(
async context =>
{
try
{
await context.Request.Body.ReadAsync(new byte[1], 0, 1);
}
catch (BadHttpRequestException)
{
}
},
expectedClientStatusCode: null,
expectedServerStatusCode: HttpStatusCode.BadRequest,
sendMalformedRequest: true);
}
private static async Task ResponseStatusCodeSetBeforeHttpContextDispose( private static async Task ResponseStatusCodeSetBeforeHttpContextDispose(
RequestDelegate handler, RequestDelegate handler,
HttpStatusCode? expectedClientStatusCode, HttpStatusCode? expectedClientStatusCode,
@ -730,14 +761,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
[Fact] [Fact]
public async Task HeadResponseCanContainContentLengthHeader() public async Task HeadResponseCanContainContentLengthHeader()
{ {
var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
using (var server = new TestServer(httpContext => using (var server = new TestServer(httpContext =>
{ {
httpContext.Response.ContentLength = 42; httpContext.Response.ContentLength = 42;
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
}, serviceContext)) }, new TestServiceContext()))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -746,7 +774,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
"", "",
""); "");
await connection.ReceiveEnd( await connection.ReceiveEnd(
$"HTTP/1.1 200 OK", "HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}", $"Date: {server.Context.DateHeaderValue}",
"Content-Length: 42", "Content-Length: 42",
"", "",
@ -758,14 +786,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
[Fact] [Fact]
public async Task HeadResponseCanContainContentLengthHeaderButBodyNotWritten() public async Task HeadResponseCanContainContentLengthHeaderButBodyNotWritten()
{ {
var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
httpContext.Response.ContentLength = 12; httpContext.Response.ContentLength = 12;
await httpContext.Response.WriteAsync("hello, world"); await httpContext.Response.WriteAsync("hello, world");
}, serviceContext)) }, new TestServiceContext()))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
@ -774,7 +799,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
"", "",
""); "");
await connection.ReceiveEnd( await connection.ReceiveEnd(
$"HTTP/1.1 200 OK", "HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}", $"Date: {server.Context.DateHeaderValue}",
"Content-Length: 12", "Content-Length: 12",
"", "",
@ -783,6 +808,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
} }
[Fact]
public async Task AppCanWriteOwnBadRequestResponse()
{
var expectedResponse = string.Empty;
var responseWrittenTcs = new TaskCompletionSource<object>();
using (var server = new TestServer(async httpContext =>
{
try
{
await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1);
}
catch (BadHttpRequestException ex)
{
expectedResponse = ex.Message;
httpContext.Response.StatusCode = 400;
httpContext.Response.ContentLength = ex.Message.Length;
await httpContext.Response.WriteAsync(ex.Message);
responseWrittenTcs.SetResult(null);
}
}, new TestServiceContext()))
{
using (var connection = server.CreateConnection())
{
await connection.SendEnd(
"POST / HTTP/1.1",
"Transfer-Encoding: chunked",
"",
"bad");
await responseWrittenTcs.Task;
await connection.ReceiveEnd(
"HTTP/1.1 400 Bad Request",
$"Date: {server.Context.DateHeaderValue}",
$"Content-Length: {expectedResponse.Length}",
"",
expectedResponse);
}
}
}
public static TheoryData<string, StringValues, string> NullHeaderData public static TheoryData<string, StringValues, string> NullHeaderData
{ {
get get