Auto-set "Content-Length: 0" on 205 responses (#7205)

This commit is contained in:
Stephen Halter 2019-02-05 13:45:08 -08:00 committed by GitHub
parent 67037a0039
commit bf24899592
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 171 additions and 54 deletions

View File

@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// Keep-alive is default for HTTP/1.1 and HTTP/2; parsing and errors will change its value
// volatile, see: https://msdn.microsoft.com/en-us/library/x13ttww7.aspx
protected volatile bool _keepAlive = true;
private bool _canHaveBody;
private bool _canWriteResponseBody;
private bool _autoChunk;
protected Exception _applicationException;
private BadHttpRequestException _requestRejectedException;
@ -828,7 +828,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
VerifyAndUpdateWrite(data.Length);
}
if (_canHaveBody)
if (_canWriteResponseBody)
{
if (_autoChunk)
{
@ -857,7 +857,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// WriteAsyncAwaited is only called for the first write to the body.
// Ensure headers are flushed if Write(Chunked)Async isn't called.
if (_canHaveBody)
if (_canWriteResponseBody)
{
if (_autoChunk)
{
@ -1140,49 +1140,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
// Set whether response can have body
_canHaveBody = StatusCanHaveBody(StatusCode) && Method != HttpMethod.Head;
_canWriteResponseBody = CanWriteResponseBody();
// Don't set the Content-Length or Transfer-Encoding headers
// automatically for HEAD requests or 204, 205, 304 responses.
if (_canHaveBody)
{
if (!hasTransferEncoding && !responseHeaders.ContentLength.HasValue)
{
if (appCompleted && StatusCode != StatusCodes.Status101SwitchingProtocols)
{
// Since the app has completed and we are only now generating
// the headers we can safely set the Content-Length to 0.
responseHeaders.ContentLength = 0;
}
else
{
// Note for future reference: never change this to set _autoChunk to true on HTTP/1.0
// connections, even if we were to infer the client supports it because an HTTP/1.0 request
// was received that used chunked encoding. Sending a chunked response to an HTTP/1.0
// client would break compliance with RFC 7230 (section 3.3.1):
//
// A server MUST NOT send a response containing Transfer-Encoding unless the corresponding
// request indicates HTTP/1.1 (or later).
//
// This also covers HTTP/2, which forbids chunked encoding in RFC 7540 (section 8.1:
//
// The chunked transfer encoding defined in Section 4.1 of [RFC7230] MUST NOT be used in HTTP/2.
if (_httpVersion == Http.HttpVersion.Http11 && StatusCode != StatusCodes.Status101SwitchingProtocols)
{
_autoChunk = true;
responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked);
}
else
{
_keepAlive = false;
}
}
}
}
else if (hasTransferEncoding)
if (!_canWriteResponseBody && hasTransferEncoding)
{
RejectNonBodyTransferEncodingResponse(appCompleted);
}
else if (!hasTransferEncoding && !responseHeaders.ContentLength.HasValue)
{
if (StatusCode == StatusCodes.Status101SwitchingProtocols)
{
_keepAlive = false;
}
else if (appCompleted || !_canWriteResponseBody)
{
// Don't set the Content-Length header automatically for HEAD requests, 204 responses, or 304 responses.
if (CanAutoSetContentLengthZeroResponseHeader())
{
// Since the app has completed writing or cannot write to the response, we can safely set the Content-Length to 0.
responseHeaders.ContentLength = 0;
}
}
// Note for future reference: never change this to set _autoChunk to true on HTTP/1.0
// connections, even if we were to infer the client supports it because an HTTP/1.0 request
// was received that used chunked encoding. Sending a chunked response to an HTTP/1.0
// client would break compliance with RFC 7230 (section 3.3.1):
//
// A server MUST NOT send a response containing Transfer-Encoding unless the corresponding
// request indicates HTTP/1.1 (or later).
//
// This also covers HTTP/2, which forbids chunked encoding in RFC 7540 (section 8.1:
//
// The chunked transfer encoding defined in Section 4.1 of [RFC7230] MUST NOT be used in HTTP/2.
else if (_httpVersion == Http.HttpVersion.Http11)
{
_autoChunk = true;
responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked);
}
else
{
_keepAlive = false;
}
}
responseHeaders.SetReadOnly();
@ -1212,12 +1211,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders);
}
public bool StatusCanHaveBody(int statusCode)
private bool CanWriteResponseBody()
{
// List of status codes taken from Microsoft.Net.Http.Server.Response
return statusCode != StatusCodes.Status204NoContent &&
statusCode != StatusCodes.Status205ResetContent &&
statusCode != StatusCodes.Status304NotModified;
return Method != HttpMethod.Head &&
StatusCode != StatusCodes.Status204NoContent &&
StatusCode != StatusCodes.Status205ResetContent &&
StatusCode != StatusCodes.Status304NotModified;
}
private bool CanAutoSetContentLengthZeroResponseHeader()
{
return Method != HttpMethod.Head &&
StatusCode != StatusCodes.Status204NoContent &&
StatusCode != StatusCodes.Status304NotModified;
}
private static void ThrowResponseAlreadyStartedException(string value)

View File

@ -477,7 +477,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
[Theory]
[InlineData(StatusCodes.Status204NoContent)]
[InlineData(StatusCodes.Status205ResetContent)]
[InlineData(StatusCodes.Status304NotModified)]
public async Task TransferEncodingChunkedNotSetOnNonBodyResponse(int statusCode)
{
@ -504,6 +503,124 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task ContentLengthZeroSetOn205Response()
{
using (var server = new TestServer(httpContext =>
{
httpContext.Response.StatusCode = 205;
return Task.CompletedTask;
}, new TestServiceContext(LoggerFactory)))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
$"HTTP/1.1 205 Reset Content",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
await server.StopAsync();
}
}
[Theory]
[InlineData(StatusCodes.Status204NoContent)]
[InlineData(StatusCodes.Status304NotModified)]
public async Task AttemptingToWriteFailsForNonBodyResponse(int statusCode)
{
var responseWriteTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.StatusCode = statusCode;
try
{
await httpContext.Response.WriteAsync("hello, world");
}
catch (Exception ex)
{
responseWriteTcs.TrySetException(ex);
throw;
}
responseWriteTcs.TrySetResult("This should not be reached.");
}, new TestServiceContext(LoggerFactory)))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => responseWriteTcs.Task).DefaultTimeout();
Assert.Equal(CoreStrings.FormatWritingToResponseBodyNotSupported(statusCode), ex.Message);
await connection.Receive(
$"HTTP/1.1 {Encoding.ASCII.GetString(ReasonPhrases.ToStatusBytes(statusCode))}",
$"Date: {server.Context.DateHeaderValue}",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task AttemptingToWriteFailsFor205Response()
{
var responseWriteTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.StatusCode = 205;
try
{
await httpContext.Response.WriteAsync("hello, world");
}
catch (Exception ex)
{
responseWriteTcs.TrySetException(ex);
throw;
}
responseWriteTcs.TrySetResult("This should not be reached.");
}, new TestServiceContext(LoggerFactory)))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => responseWriteTcs.Task).DefaultTimeout();
Assert.Equal(CoreStrings.FormatWritingToResponseBodyNotSupported(205), ex.Message);
await connection.Receive(
$"HTTP/1.1 205 Reset Content",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task TransferEncodingNotSetOnHeadResponse()
{
@ -1850,10 +1967,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
"Host:",
"Content-Length: 3",
"",
"205POST / HTTP/1.1",
"Host:",
"Content-Length: 3",
"",
"304POST / HTTP/1.1",
"Host:",
"Content-Length: 3",
@ -1863,9 +1976,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
"HTTP/1.1 204 No Content",
$"Date: {testContext.DateHeaderValue}",
"",
"HTTP/1.1 205 Reset Content",
$"Date: {testContext.DateHeaderValue}",
"",
"HTTP/1.1 304 Not Modified",
$"Date: {testContext.DateHeaderValue}",
"",