Refactor non-body response handling.

- Add functional tests
- Remove BadHttpResponse, ResponseRejectionReasons and TestFrameProtectedMembers
- Chunk non-keepalive HTTP/1.1 responses
- Set _canHaveBody to true when StatusCode == 101
- Add test to check that upgraded connections are always closed when the app
  is done
- Log writes on responses to HEAD requests only once.
This commit is contained in:
Cesar Blum Silveira 2016-09-30 12:17:31 -07:00
parent e7e6b896ba
commit 4117ad8a1e
11 changed files with 362 additions and 295 deletions

View File

@ -1,75 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
namespace Microsoft.AspNetCore.Server.Kestrel
{
public static class BadHttpResponse
{
internal static void ThrowException(ResponseRejectionReasons reason)
{
throw GetException(reason);
}
internal static void ThrowException(ResponseRejectionReasons reason, int value)
{
throw GetException(reason, value.ToString());
}
internal static void ThrowException(ResponseRejectionReasons reason, ResponseRejectionParameter parameter)
{
throw GetException(reason, parameter.ToString());
}
internal static InvalidOperationException GetException(ResponseRejectionReasons reason, int value)
{
return GetException(reason, value.ToString());
}
[MethodImpl(MethodImplOptions.NoInlining)]
internal static InvalidOperationException GetException(ResponseRejectionReasons reason)
{
InvalidOperationException ex;
switch (reason)
{
case ResponseRejectionReasons.HeadersReadonlyResponseStarted:
ex = new InvalidOperationException("Headers are read-only, response has already started.");
break;
case ResponseRejectionReasons.OnStartingCannotBeSetResponseStarted:
ex = new InvalidOperationException("OnStarting cannot be set, response has already started.");
break;
default:
ex = new InvalidOperationException("Bad response.");
break;
}
return ex;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static InvalidOperationException GetException(ResponseRejectionReasons reason, string value)
{
InvalidOperationException ex;
switch (reason)
{
case ResponseRejectionReasons.ValueCannotBeSetResponseStarted:
ex = new InvalidOperationException(value + " cannot be set, response had already started.");
break;
case ResponseRejectionReasons.TransferEncodingSetOnNonBodyResponse:
ex = new InvalidOperationException($"Transfer-Encoding set on a {value} non-body request.");
break;
case ResponseRejectionReasons.WriteToNonBodyResponse:
ex = new InvalidOperationException($"Write to non-body {value} response.");
break;
default:
ex = new InvalidOperationException("Bad response.");
break;
}
return ex;
}
}
}

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -74,6 +75,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
protected readonly long _keepAliveMilliseconds; protected readonly long _keepAliveMilliseconds;
private readonly long _requestHeadersTimeoutMilliseconds; private readonly long _requestHeadersTimeoutMilliseconds;
private int _responseBytesWritten;
public Frame(ConnectionContext context) public Frame(ConnectionContext context)
{ {
ConnectionContext = context; ConnectionContext = context;
@ -172,7 +175,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
if (HasResponseStarted) if (HasResponseStarted)
{ {
BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.StatusCode); ThrowResponseAlreadyStartedException(nameof(StatusCode));
} }
_statusCode = value; _statusCode = value;
@ -190,7 +193,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
if (HasResponseStarted) if (HasResponseStarted)
{ {
BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.ReasonPhrase); ThrowResponseAlreadyStartedException(nameof(ReasonPhrase));
} }
_reasonPhrase = value; _reasonPhrase = value;
@ -346,6 +349,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize; _remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize;
_requestHeadersParsed = 0; _requestHeadersParsed = 0;
_responseBytesWritten = 0;
} }
/// <summary> /// <summary>
@ -426,7 +431,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
if (HasResponseStarted) if (HasResponseStarted)
{ {
BadHttpResponse.ThrowException(ResponseRejectionReasons.OnStartingCannotBeSetResponseStarted, ResponseRejectionParameter.OnStarting); ThrowResponseAlreadyStartedException(nameof(OnStarting));
} }
if (_onStarting == null) if (_onStarting == null)
@ -512,6 +517,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public void Write(ArraySegment<byte> data) public void Write(ArraySegment<byte> data)
{ {
ProduceStartAndFireOnStarting().GetAwaiter().GetResult(); ProduceStartAndFireOnStarting().GetAwaiter().GetResult();
_responseBytesWritten += data.Count;
if (_canHaveBody) if (_canHaveBody)
{ {
@ -530,7 +536,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
else else
{ {
HandleNonBodyResponseWrite(data.Count); HandleNonBodyResponseWrite();
} }
} }
@ -541,6 +547,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return WriteAsyncAwaited(data, cancellationToken); return WriteAsyncAwaited(data, cancellationToken);
} }
_responseBytesWritten += data.Count;
if (_canHaveBody) if (_canHaveBody)
{ {
if (_autoChunk) if (_autoChunk)
@ -558,7 +566,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
else else
{ {
HandleNonBodyResponseWrite(data.Count); HandleNonBodyResponseWrite();
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
} }
} }
@ -566,6 +574,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public async Task WriteAsyncAwaited(ArraySegment<byte> data, CancellationToken cancellationToken) public async Task WriteAsyncAwaited(ArraySegment<byte> data, CancellationToken cancellationToken)
{ {
await ProduceStartAndFireOnStarting(); await ProduceStartAndFireOnStarting();
_responseBytesWritten += data.Count;
if (_canHaveBody) if (_canHaveBody)
{ {
@ -584,10 +593,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
else else
{ {
HandleNonBodyResponseWrite(data.Count); HandleNonBodyResponseWrite();
return; return;
} }
} }
private void WriteChunked(ArraySegment<byte> data) private void WriteChunked(ArraySegment<byte> data)
@ -700,11 +708,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
} }
// If the request was rejected, StatusCode has already been set by SetBadRequestState // If the request was rejected, the error state has already been set by SetBadRequestState
if (!_requestRejected) if (!_requestRejected)
{ {
// 500 Internal Server Error // 500 Internal Server Error
ErrorResetHeadersToDefaults(statusCode: 500); SetErrorResponseHeaders(statusCode: 500);
} }
} }
@ -740,6 +748,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
ConnectionControl.End(ProduceEndType.ConnectionKeepAlive); ConnectionControl.End(ProduceEndType.ConnectionKeepAlive);
} }
if (HttpMethods.IsHead(Method) && _responseBytesWritten > 0)
{
Log.ConnectionHeadResponseBodyWrite(ConnectionId, _responseBytesWritten);
}
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
} }
@ -758,24 +771,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
bool appCompleted) bool appCompleted)
{ {
var responseHeaders = FrameResponseHeaders; var responseHeaders = FrameResponseHeaders;
var hasConnection = responseHeaders.HasConnection; var hasConnection = responseHeaders.HasConnection;
// Set whether response can have body
_canHaveBody = StatusCanHaveBody(StatusCode) && Method != "HEAD";
var end = SocketOutput.ProducingStart(); var end = SocketOutput.ProducingStart();
if (_keepAlive && hasConnection) if (_keepAlive && hasConnection)
{ {
var connectionValue = responseHeaders.HeaderConnection.ToString(); var connectionValue = responseHeaders.HeaderConnection.ToString();
_keepAlive = connectionValue.Equals("keep-alive", StringComparison.OrdinalIgnoreCase); _keepAlive = connectionValue.Equals("keep-alive", StringComparison.OrdinalIgnoreCase);
} }
// Set whether response can have body
_canHaveBody = StatusCanHaveBody(StatusCode) && Method != "HEAD";
// Don't set the Content-Length or Transfer-Encoding headers
// automatically for HEAD requests or 204, 205, 304 responses.
if (_canHaveBody) if (_canHaveBody)
{ {
if (!responseHeaders.HasTransferEncoding && !responseHeaders.HasContentLength) if (!responseHeaders.HasTransferEncoding && !responseHeaders.HasContentLength)
{ {
if (appCompleted) if (appCompleted && StatusCode != 101)
{ {
// Since the app has completed and we are only now generating // Since the app has completed and we are only now generating
// the headers we can safely set the Content-Length to 0. // the headers we can safely set the Content-Length to 0.
@ -790,7 +805,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// //
// A server MUST NOT send a response containing Transfer-Encoding unless the corresponding // A server MUST NOT send a response containing Transfer-Encoding unless the corresponding
// request indicates HTTP/1.1 (or later). // request indicates HTTP/1.1 (or later).
if (_httpVersion == Http.HttpVersion.Http11) if (_httpVersion == Http.HttpVersion.Http11 && StatusCode != 101)
{ {
_autoChunk = true; _autoChunk = true;
responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked); responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked);
@ -804,8 +819,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
else else
{ {
// Don't set the Content-Length or Transfer-Encoding headers
// automatically for HEAD requests or 101, 204, 205, 304 responses.
if (responseHeaders.HasTransferEncoding) if (responseHeaders.HasTransferEncoding)
{ {
RejectNonBodyTransferEncodingResponse(appCompleted); RejectNonBodyTransferEncodingResponse(appCompleted);
@ -1271,15 +1284,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public bool StatusCanHaveBody(int statusCode) public bool StatusCanHaveBody(int statusCode)
{ {
// List of status codes taken from Microsoft.Net.Http.Server.Response // List of status codes taken from Microsoft.Net.Http.Server.Response
return statusCode != 101 && return statusCode != 204 &&
statusCode != 204 &&
statusCode != 205 && statusCode != 205 &&
statusCode != 304; statusCode != 304;
} }
private void ThrowResponseAlreadyStartedException(string value)
{
throw new InvalidOperationException($"{value} cannot be set, response has already started.");
}
private void RejectNonBodyTransferEncodingResponse(bool appCompleted) private void RejectNonBodyTransferEncodingResponse(bool appCompleted)
{ {
var ex = BadHttpResponse.GetException(ResponseRejectionReasons.TransferEncodingSetOnNonBodyResponse, StatusCode); var ex = new InvalidOperationException($"Transfer-Encoding set on a {StatusCode} non-body request.");
if (!appCompleted) if (!appCompleted)
{ {
// Back out of header creation surface exeception in user code // Back out of header creation surface exeception in user code
@ -1289,18 +1306,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
else else
{ {
ReportApplicationError(ex); ReportApplicationError(ex);
// 500 Internal Server Error // 500 Internal Server Error
ErrorResetHeadersToDefaults(statusCode: 500); SetErrorResponseHeaders(statusCode: 500);
} }
} }
private void ErrorResetHeadersToDefaults(int statusCode) private void SetErrorResponseHeaders(int statusCode)
{ {
// Setting status code will throw if response has already started Debug.Assert(!HasResponseStarted, $"{nameof(SetErrorResponseHeaders)} called after response had already started.");
if (!HasResponseStarted)
StatusCode = statusCode;
ReasonPhrase = null;
if (FrameResponseHeaders == null)
{ {
StatusCode = statusCode; InitializeHeaders();
ReasonPhrase = null;
} }
var responseHeaders = FrameResponseHeaders; var responseHeaders = FrameResponseHeaders;
@ -1316,17 +1337,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
} }
public void HandleNonBodyResponseWrite(int count) public void HandleNonBodyResponseWrite()
{ {
if (Method == "HEAD") // Writes to HEAD response are ignored and logged at the end of the request
if (Method != "HEAD")
{ {
// Don't write to body for HEAD requests. // Throw Exception for 204, 205, 304 responses.
Log.ConnectionHeadResponseBodyWrite(ConnectionId, count); throw new InvalidOperationException($"Write to non-body {StatusCode} response.");
}
else
{
// Throw Exception for 101, 204, 205, 304 responses.
BadHttpResponse.ThrowException(ResponseRejectionReasons.WriteToNonBodyResponse, StatusCode);
} }
} }
@ -1365,7 +1382,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public void SetBadRequestState(BadHttpRequestException ex) public void SetBadRequestState(BadHttpRequestException ex)
{ {
ErrorResetHeadersToDefaults(statusCode: ex.StatusCode); // Setting status code will throw if response has already started
if (!HasResponseStarted)
{
SetErrorResponseHeaders(statusCode: ex.StatusCode);
}
_keepAlive = false; _keepAlive = false;
_requestProcessingStopping = true; _requestProcessingStopping = true;

View File

@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
if (_isReadOnly) if (_isReadOnly)
{ {
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); ThrowHeadersReadOnlyException();
} }
SetValueFast(key, value); SetValueFast(key, value);
} }
@ -48,6 +48,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
} }
protected void ThrowHeadersReadOnlyException()
{
throw new InvalidOperationException("Headers are read-only, response has already started.");
}
protected void ThrowArgumentException() protected void ThrowArgumentException()
{ {
throw new ArgumentException(); throw new ArgumentException();
@ -139,7 +144,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
if (_isReadOnly) if (_isReadOnly)
{ {
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); ThrowHeadersReadOnlyException();
} }
AddValueFast(key, value); AddValueFast(key, value);
} }
@ -148,7 +153,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
if (_isReadOnly) if (_isReadOnly)
{ {
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); ThrowHeadersReadOnlyException();
} }
ClearFast(); ClearFast();
} }
@ -195,7 +200,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
if (_isReadOnly) if (_isReadOnly)
{ {
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); ThrowHeadersReadOnlyException();
} }
return RemoveFast(key); return RemoveFast(key);
} }

View File

@ -1,22 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
public enum ResponseRejectionReasons
{
HeadersReadonlyResponseStarted,
ValueCannotBeSetResponseStarted,
TransferEncodingSetOnNonBodyResponse,
WriteToNonBodyResponse,
OnStartingCannotBeSetResponseStarted
}
public enum ResponseRejectionParameter
{
StatusCode,
ReasonPhrase,
OnStarting
}
}

View File

@ -5,13 +5,17 @@ using System;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
@ -213,6 +217,116 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
} }
[Fact]
public async Task TransferEncodingChunkedSetOnUnknownLengthHttp11Response()
{
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.WriteAsync("hello, ");
await httpContext.Response.WriteAsync("world");
}, new TestServiceContext()))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"7",
"hello, ",
"5",
"world",
"0",
"",
"");
}
}
}
[Theory]
[InlineData(204)]
[InlineData(205)]
[InlineData(304)]
public async Task TransferEncodingChunkedNotSetOnNonBodyResponse(int statusCode)
{
using (var server = new TestServer(httpContext =>
{
httpContext.Response.StatusCode = statusCode;
return TaskCache.CompletedTask;
}, new TestServiceContext()))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.Receive(
$"HTTP/1.1 {Encoding.ASCII.GetString(ReasonPhrases.ToStatusBytes(statusCode))}",
$"Date: {server.Context.DateHeaderValue}",
"",
"");
}
}
}
[Fact]
public async Task TransferEncodingNotSetOnHeadResponse()
{
using (var server = new TestServer(httpContext =>
{
return TaskCache.CompletedTask;
}, new TestServiceContext()))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"HEAD / HTTP/1.1",
"",
"");
await connection.Receive(
$"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"",
"");
}
}
}
[Fact]
public async Task ResponseBodyNotWrittenOnHeadResponse()
{
var mockKestrelTrace = new Mock<IKestrelTrace>();
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.WriteAsync("hello, world");
await httpContext.Response.Body.FlushAsync();
}, new TestServiceContext { Log = mockKestrelTrace.Object }))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"HEAD / HTTP/1.1",
"",
"");
await connection.Receive(
$"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"",
"");
}
}
mockKestrelTrace.Verify(kestrelTrace =>
kestrelTrace.ConnectionHeadResponseBodyWrite(It.IsAny<string>(), "hello, world".Length));
}
public static TheoryData<string, StringValues, string> NullHeaderData public static TheoryData<string, StringValues, string> NullHeaderData
{ {
get get

View File

@ -7,6 +7,7 @@
"Microsoft.AspNetCore.Server.Kestrel.Https": "1.1.0-*", "Microsoft.AspNetCore.Server.Kestrel.Https": "1.1.0-*",
"Microsoft.AspNetCore.Testing": "1.1.0-*", "Microsoft.AspNetCore.Testing": "1.1.0-*",
"Microsoft.Extensions.Logging.Testing": "1.1.0-*", "Microsoft.Extensions.Logging.Testing": "1.1.0-*",
"Moq": "4.6.36-*",
"Newtonsoft.Json": "9.0.1", "Newtonsoft.Json": "9.0.1",
"xunit": "2.2.0-*" "xunit": "2.2.0-*"
}, },

View File

@ -5,6 +5,7 @@ using System;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;
@ -63,13 +64,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionFilterData))]
public async Task ResponsesAreNotChunkedAutomaticallyForHttp10RequestsAndHttp11NonKeepAliveRequests(TestServiceContext testContext) public async Task ResponsesAreNotChunkedAutomaticallyForHttp10Requests(TestServiceContext testContext)
{ {
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var response = httpContext.Response; await httpContext.Response.WriteAsync("Hello ");
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6); await httpContext.Response.WriteAsync("World!");
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6);
}, testContext)) }, testContext))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
@ -86,7 +86,19 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"", "",
"Hello World!"); "Hello World!");
} }
}
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ResponsesAreChunkedAutomaticallyForHttp11NonKeepAliveRequests(TestServiceContext testContext)
{
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.WriteAsync("Hello ");
await httpContext.Response.WriteAsync("World!");
}, testContext))
{
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
await connection.SendEnd( await connection.SendEnd(
@ -98,23 +110,28 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"HTTP/1.1 200 OK", "HTTP/1.1 200 OK",
"Connection: close", "Connection: close",
$"Date: {testContext.DateHeaderValue}", $"Date: {testContext.DateHeaderValue}",
"Transfer-Encoding: chunked",
"", "",
"Hello World!"); "6",
"Hello ",
"6",
"World!",
"0",
"",
"");
} }
} }
} }
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionFilterData))]
public async Task SettingConnectionCloseHeaderInAppDisablesChunking(TestServiceContext testContext) public async Task SettingConnectionCloseHeaderInAppDoesNotDisableChunking(TestServiceContext testContext)
{ {
using (var server = new TestServer(async httpContext => using (var server = new TestServer(async httpContext =>
{ {
var response = httpContext.Response; httpContext.Response.Headers["Connection"] = "close";
response.Headers["Connection"] = "close"; await httpContext.Response.WriteAsync("Hello ");
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6); await httpContext.Response.WriteAsync("World!");
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6);
}, testContext)) }, testContext))
{ {
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
@ -127,8 +144,15 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"HTTP/1.1 200 OK", "HTTP/1.1 200 OK",
"Connection: close", "Connection: close",
$"Date: {testContext.DateHeaderValue}", $"Date: {testContext.DateHeaderValue}",
"Transfer-Encoding: chunked",
"", "",
"Hello World!"); "6",
"Hello ",
"6",
"World!",
"0",
"",
"");
} }
} }
} }

View File

@ -548,9 +548,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"POST / HTTP/1.1", "POST / HTTP/1.1",
"Content-Length: 3", "Content-Length: 3",
"", "",
"101POST / HTTP/1.1",
"Content-Length: 3",
"",
"204POST / HTTP/1.1", "204POST / HTTP/1.1",
"Content-Length: 3", "Content-Length: 3",
"", "",
@ -562,9 +559,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"", "",
"200"); "200");
await connection.ReceiveEnd( await connection.ReceiveEnd(
"HTTP/1.1 101 Switching Protocols",
$"Date: {testContext.DateHeaderValue}",
"",
"HTTP/1.1 204 No Content", "HTTP/1.1 204 No Content",
$"Date: {testContext.DateHeaderValue}", $"Date: {testContext.DateHeaderValue}",
"", "",
@ -583,6 +577,78 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
} }
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ConnectionClosedAfter101Response(TestServiceContext testContext)
{
using (var server = new TestServer(async httpContext =>
{
var request = httpContext.Request;
var stream = await httpContext.Features.Get<IHttpUpgradeFeature>().UpgradeAsync();
var response = Encoding.ASCII.GetBytes("hello, world");
await stream.WriteAsync(response, 0, response.Length);
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 101 Switching Protocols",
"Connection: Upgrade",
$"Date: {testContext.DateHeaderValue}",
"",
"hello, world");
}
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.0",
"Connection: keep-alive",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 101 Switching Protocols",
"Connection: Upgrade",
$"Date: {testContext.DateHeaderValue}",
"",
"hello, world");
}
}
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task WriteOnHeadResponseLoggedOnlyOnce(TestServiceContext testContext)
{
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.WriteAsync("hello, ");
await httpContext.Response.WriteAsync("world");
await httpContext.Response.WriteAsync("!");
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.SendEnd(
"HEAD / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"",
"");
}
Assert.Equal(1, ((TestKestrelTrace)testContext.Log).HeadResponseWrites);
Assert.Equal(13, ((TestKestrelTrace)testContext.Log).HeadResponseWriteByteCount);
}
}
[Theory] [Theory]
[MemberData(nameof(ConnectionFilterData))] [MemberData(nameof(ConnectionFilterData))]
public async Task ThrowingResultsIn500Response(TestServiceContext testContext) public async Task ThrowingResultsIn500Response(TestServiceContext testContext)

View File

@ -5,6 +5,7 @@ using System;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal;
@ -1263,7 +1264,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
[Fact] [Fact]
public void FlushSetsTransferEncodingSetForUnknownLengthBodyResponse() public void WriteThrowsForNonBodyResponse()
{ {
// Arrange // Arrange
var serviceContext = new ServiceContext var serviceContext = new ServiceContext
@ -1280,114 +1281,40 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
SocketOutput = new MockSocketOuptut(), SocketOutput = new MockSocketOuptut(),
}; };
var frame = new TestFrameProtectedMembers<object>(application: null, context: connectionContext); var frame = new Frame<object>(application: null, context: connectionContext);
frame.InitializeHeaders(); frame.InitializeHeaders();
frame.KeepAlive = true;
frame.HttpVersion = "HTTP/1.1";
// Act
frame.Flush();
// Assert
Assert.True(frame.HasResponseStarted);
Assert.True(frame.ResponseHeaders.ContainsKey("Transfer-Encoding"));
}
[Fact]
public void FlushDoesNotSetTransferEncodingSetForNoBodyResponse()
{
// Arrange
var serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new TestFrameProtectedMembers<object>(application: null, context: connectionContext);
frame.InitializeHeaders();
frame.KeepAlive = true;
frame.HttpVersion = "HTTP/1.1"; frame.HttpVersion = "HTTP/1.1";
((IHttpResponseFeature)frame).StatusCode = 304; ((IHttpResponseFeature)frame).StatusCode = 304;
// Act // Act/Assert
frame.Flush();
// Assert
Assert.True(frame.HasResponseStarted);
Assert.False(frame.ResponseHeaders.ContainsKey("Transfer-Encoding"));
}
[Fact]
public void FlushDoesNotSetTransferEncodingSetForHeadResponse()
{
// Arrange
var serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new TestFrameProtectedMembers<object>(application: null, context: connectionContext);
frame.InitializeHeaders();
frame.KeepAlive = true;
frame.HttpVersion = "HTTP/1.1";
((IHttpRequestFeature)frame).Method = "HEAD";
// Act
frame.Flush();
// Assert
Assert.True(frame.HasResponseStarted);
Assert.False(frame.ResponseHeaders.ContainsKey("Transfer-Encoding"));
}
[Fact]
public void WriteThrowsForNoBodyResponse()
{
// Arrange
var serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new TestFrameProtectedMembers<object>(application: null, context: connectionContext);
frame.InitializeHeaders();
frame.KeepAlive = true;
frame.HttpVersion = "HTTP/1.1";
((IHttpResponseFeature)frame).StatusCode = 304;
// Assert
frame.Flush(); // Does not throw
Assert.Throws<InvalidOperationException>(() => frame.Write(new ArraySegment<byte>(new byte[1]))); Assert.Throws<InvalidOperationException>(() => frame.Write(new ArraySegment<byte>(new byte[1])));
Assert.ThrowsAsync<InvalidOperationException>(() => frame.WriteAsync(new ArraySegment<byte>(new byte[1]), default(CancellationToken))); }
frame.Flush(); // Does not throw [Fact]
public async Task WriteAsyncThrowsForNonBodyResponse()
{
// Arrange
var serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new Frame<object>(application: null, context: connectionContext);
frame.InitializeHeaders();
frame.HttpVersion = "HTTP/1.1";
((IHttpResponseFeature)frame).StatusCode = 304;
// Act/Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => frame.WriteAsync(new ArraySegment<byte>(new byte[1]), default(CancellationToken)));
} }
[Fact] [Fact]
@ -1408,20 +1335,41 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
SocketOutput = new MockSocketOuptut(), SocketOutput = new MockSocketOuptut(),
}; };
var frame = new TestFrameProtectedMembers<object>(application: null, context: connectionContext); var frame = new Frame<object>(application: null, context: connectionContext);
frame.InitializeHeaders(); frame.InitializeHeaders();
frame.KeepAlive = true;
frame.HttpVersion = "HTTP/1.1"; frame.HttpVersion = "HTTP/1.1";
((IHttpRequestFeature)frame).Method = "HEAD"; ((IHttpRequestFeature)frame).Method = "HEAD";
// Assert // Act/Assert
frame.Flush(); // Does not throw
frame.Write(new ArraySegment<byte>(new byte[1])); frame.Write(new ArraySegment<byte>(new byte[1]));
frame.Flush(); // Does not throw
} }
[Fact]
public async Task WriteAsyncDoesNotThrowForHeadResponse()
{
// Arrange
var serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new Frame<object>(application: null, context: connectionContext);
frame.InitializeHeaders();
frame.HttpVersion = "HTTP/1.1";
((IHttpRequestFeature)frame).Method = "HEAD";
// Act/Assert
await frame.WriteAsync(new ArraySegment<byte>(new byte[1]), default(CancellationToken));
}
[Fact] [Fact]
public void ManuallySettingTransferEncodingThrowsForHeadResponse() public void ManuallySettingTransferEncodingThrowsForHeadResponse()
@ -1441,13 +1389,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
SocketOutput = new MockSocketOuptut(), SocketOutput = new MockSocketOuptut(),
}; };
var frame = new TestFrameProtectedMembers<object>(application: null, context: connectionContext); var frame = new Frame<object>(application: null, context: connectionContext);
frame.InitializeHeaders(); frame.InitializeHeaders();
frame.KeepAlive = true;
frame.HttpVersion = "HTTP/1.1"; frame.HttpVersion = "HTTP/1.1";
((IHttpRequestFeature)frame).Method = "HEAD"; ((IHttpRequestFeature)frame).Method = "HEAD";
//Act // Act
frame.ResponseHeaders.Add("Transfer-Encoding", "chunked"); frame.ResponseHeaders.Add("Transfer-Encoding", "chunked");
// Assert // Assert
@ -1472,13 +1419,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
SocketOutput = new MockSocketOuptut(), SocketOutput = new MockSocketOuptut(),
}; };
var frame = new TestFrameProtectedMembers<object>(application: null, context: connectionContext); var frame = new Frame<object>(application: null, context: connectionContext);
frame.InitializeHeaders(); frame.InitializeHeaders();
frame.KeepAlive = true;
frame.HttpVersion = "HTTP/1.1"; frame.HttpVersion = "HTTP/1.1";
((IHttpResponseFeature)frame).StatusCode = 304; ((IHttpResponseFeature)frame).StatusCode = 304;
//Act // Act
frame.ResponseHeaders.Add("Transfer-Encoding", "chunked"); frame.ResponseHeaders.Add("Transfer-Encoding", "chunked");
// Assert // Assert

View File

@ -1,23 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
public class TestFrameProtectedMembers<TContext> : Frame<TContext>
{
public TestFrameProtectedMembers(IHttpApplication<TContext> application, ConnectionContext context)
: base(application, context)
{
}
public bool KeepAlive
{
get { return _keepAlive; }
set { _keepAlive = value; }
}
}
}

View File

@ -13,6 +13,10 @@ namespace Microsoft.AspNetCore.Testing
{ {
} }
public int HeadResponseWrites { get; set; }
public int HeadResponseWriteByteCount { get; set; }
public override void ConnectionRead(string connectionId, int count) public override void ConnectionRead(string connectionId, int count)
{ {
//_logger.LogDebug(1, @"Connection id ""{ConnectionId}"" recv {count} bytes.", connectionId, count); //_logger.LogDebug(1, @"Connection id ""{ConnectionId}"" recv {count} bytes.", connectionId, count);
@ -27,5 +31,11 @@ namespace Microsoft.AspNetCore.Testing
{ {
//_logger.LogDebug(1, @"Connection id ""{ConnectionId}"" send finished with status {status}.", connectionId, status); //_logger.LogDebug(1, @"Connection id ""{ConnectionId}"" send finished with status {status}.", connectionId, status);
} }
public override void ConnectionHeadResponseBodyWrite(string connectionId, int count)
{
HeadResponseWrites++;
HeadResponseWriteByteCount = count;
}
} }
} }