Merge branch 'release' into dev

This commit is contained in:
Stephen Halter 2015-08-26 16:32:19 -07:00
commit 40ca61b640
3 changed files with 286 additions and 13 deletions

View File

@ -254,11 +254,6 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{ {
FireOnStarting(); FireOnStarting();
} }
if (_autoChunk)
{
WriteChunkedResponseSuffix();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -380,7 +375,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
} }
} }
public void ProduceStart(bool immediate = true) public void ProduceStart(bool immediate = true, bool appCompleted = false)
{ {
// ProduceStart shouldn't no-op in the future just b/c FireOnStarting throws. // ProduceStart shouldn't no-op in the future just b/c FireOnStarting throws.
if (_responseStarted) return; if (_responseStarted) return;
@ -389,7 +384,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
var status = ReasonPhrases.ToStatus(StatusCode, ReasonPhrase); var status = ReasonPhrases.ToStatus(StatusCode, ReasonPhrase);
var responseHeader = CreateResponseHeader(status, ResponseHeaders); var responseHeader = CreateResponseHeader(status, appCompleted, ResponseHeaders);
SocketOutput.Write( SocketOutput.Write(
responseHeader.Item1, responseHeader.Item1,
(error, x) => (error, x) =>
@ -428,7 +423,14 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
} }
} }
ProduceStart(); ProduceStart(immediate: true, appCompleted: true);
// _autoChunk should be checked after we are sure ProduceStart() has been called
// since ProduceStart() may set _autoChunk to true.
if (_autoChunk)
{
WriteChunkedResponseSuffix();
}
if (!_keepAlive) if (!_keepAlive)
{ {
@ -440,7 +442,9 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
} }
private Tuple<ArraySegment<byte>, IDisposable> CreateResponseHeader( private Tuple<ArraySegment<byte>, IDisposable> CreateResponseHeader(
string status, IEnumerable<KeyValuePair<string, string[]>> headers) string status,
bool appCompleted,
IEnumerable<KeyValuePair<string, string[]>> headers)
{ {
var writer = new MemoryPoolTextWriter(Memory); var writer = new MemoryPoolTextWriter(Memory);
writer.Write(HttpVersion); writer.Write(HttpVersion);
@ -492,16 +496,31 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
if (_keepAlive && !hasTransferEncoding && !hasContentLength) if (_keepAlive && !hasTransferEncoding && !hasContentLength)
{ {
if (HttpVersion == "HTTP/1.1") if (appCompleted)
{ {
_autoChunk = true; // Don't set the Content-Length or Transfer-Encoding headers
writer.Write("Transfer-Encoding: chunked\r\n"); // automatically for HEAD requests or 101, 204, 205, 304 responses.
if (Method != "HEAD" && StatusCanHaveBody(StatusCode))
{
// Since the app has completed and we are only now generating
// the headers we can safely set the Content-Length to 0.
writer.Write("Content-Length: 0\r\n");
}
} }
else else
{ {
_keepAlive = false; if (HttpVersion == "HTTP/1.1")
{
_autoChunk = true;
writer.Write("Transfer-Encoding: chunked\r\n");
}
else
{
_keepAlive = false;
}
} }
} }
if (_keepAlive == false && hasConnection == false && HttpVersion == "HTTP/1.1") if (_keepAlive == false && hasConnection == false && HttpVersion == "HTTP/1.1")
{ {
writer.Write("Connection: close\r\n\r\n"); writer.Write("Connection: close\r\n\r\n");
@ -661,5 +680,14 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{ {
_requestHeaders.Append(keyBytes, keyOffset, keyLength, value); _requestHeaders.Append(keyBytes, keyOffset, keyLength, value);
} }
public bool StatusCanHaveBody(int statusCode)
{
// List of status codes taken from Microsoft.Net.Http.Server.Response
return statusCode != 101 &&
statusCode != 204 &&
statusCode != 205 &&
statusCode != 304;
}
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
@ -71,5 +72,85 @@ namespace Microsoft.AspNet.Server.KestrelTests
} }
} }
} }
[Fact]
public async Task EmptyResponseBodyHandledCorrectlyWithZeroLengthWrite()
{
using (var server = new TestServer(async frame =>
{
frame.ResponseHeaders.Clear();
await frame.ResponseBody.WriteAsync(new byte[0], 0, 0);
}))
{
using (var connection = new TestConnection())
{
await connection.SendEnd(
"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
"Transfer-Encoding: chunked",
"",
"0",
"",
"");
}
}
}
[Fact]
public async Task ConnectionClosedIfExeptionThrownAfterWrite()
{
using (var server = new TestServer(async frame =>
{
frame.ResponseHeaders.Clear();
await frame.ResponseBody.WriteAsync(Encoding.ASCII.GetBytes("Hello World!"), 0, 12);
throw new Exception();
}))
{
using (var connection = new TestConnection())
{
// SendEnd is not called, so it isn't the client closing the connection.
// client closing the connection.
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
"Transfer-Encoding: chunked",
"",
"c",
"Hello World!",
"");
}
}
}
[Fact]
public async Task ConnectionClosedIfExeptionThrownAfterZeroLengthWrite()
{
using (var server = new TestServer(async frame =>
{
frame.ResponseHeaders.Clear();
await frame.ResponseBody.WriteAsync(new byte[0], 0, 0);
throw new Exception();
}))
{
using (var connection = new TestConnection())
{
// SendEnd is not called, so it isn't the client closing the connection.
await connection.Send(
"GET / HTTP/1.1",
"",
"");
// Nothing (not even headers) are written, but the connection is closed.
await connection.ReceiveEnd();
}
}
}
} }
} }

View File

@ -77,6 +77,12 @@ namespace Microsoft.AspNet.Server.KestrelTests
await frame.ResponseBody.WriteAsync(bytes, 0, bytes.Length); await frame.ResponseBody.WriteAsync(bytes, 0, bytes.Length);
} }
private Task EmptyApp(Frame frame)
{
frame.ResponseHeaders.Clear();
return Task.FromResult<object>(null);
}
[Fact] [Fact]
public void EngineCanStartAndStop() public void EngineCanStartAndStop()
{ {
@ -235,6 +241,35 @@ namespace Microsoft.AspNet.Server.KestrelTests
} }
} }
[Fact]
public async Task Http10KeepAliveNotUsedIfResponseContentLengthNotSet()
{
using (var server = new TestServer(App))
{
using (var connection = new TestConnection())
{
await connection.SendEnd(
"GET / HTTP/1.0",
"Connection: keep-alive",
"",
"POST / HTTP/1.0",
"Connection: keep-alive",
"Content-Length: 7",
"",
"Goodbye");
await connection.Receive(
"HTTP/1.0 200 OK",
"Content-Length: 0",
"Connection: keep-alive",
"\r\n");
await connection.ReceiveEnd(
"HTTP/1.0 200 OK",
"",
"Goodbye");
}
}
}
[Fact] [Fact]
public async Task Http10KeepAliveContentLength() public async Task Http10KeepAliveContentLength()
{ {
@ -346,6 +381,135 @@ namespace Microsoft.AspNet.Server.KestrelTests
} }
} }
[Fact]
public async Task ZeroContentLengthSetAutomaticallyAfterNoWrites()
{
using (var server = new TestServer(EmptyApp))
{
using (var connection = new TestConnection())
{
await connection.SendEnd(
"GET / HTTP/1.1",
"",
"GET / HTTP/1.0",
"Connection: keep-alive",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
"Content-Length: 0",
"",
"HTTP/1.0 200 OK",
"Content-Length: 0",
"Connection: keep-alive",
"",
"");
}
}
}
[Fact]
public async Task ZeroContentLengthNotSetAutomaticallyForNonKeepAliveRequests()
{
using (var server = new TestServer(EmptyApp))
{
using (var connection = new TestConnection())
{
await connection.SendEnd(
"GET / HTTP/1.1",
"Connection: close",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
"Connection: close",
"",
"");
}
using (var connection = new TestConnection())
{
await connection.SendEnd(
"GET / HTTP/1.0",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.0 200 OK",
"",
"");
}
}
}
[Fact]
public async Task ZeroContentLengthNotSetAutomaticallyForHeadRequests()
{
using (var server = new TestServer(EmptyApp))
{
using (var connection = new TestConnection())
{
await connection.SendEnd(
"HEAD / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
"",
"");
}
}
}
[Fact]
public async Task ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes()
{
using (var server = new TestServer(async frame =>
{
frame.ResponseHeaders.Clear();
using (var reader = new StreamReader(frame.RequestBody, Encoding.ASCII))
{
var statusString = await reader.ReadLineAsync();
frame.StatusCode = int.Parse(statusString);
}
}))
{
using (var connection = new TestConnection())
{
await connection.SendEnd(
"POST / HTTP/1.1",
"Content-Length: 3",
"",
"101POST / HTTP/1.1",
"Content-Length: 3",
"",
"204POST / HTTP/1.1",
"Content-Length: 3",
"",
"205POST / HTTP/1.1",
"Content-Length: 3",
"",
"304POST / HTTP/1.1",
"Content-Length: 3",
"",
"200");
await connection.ReceiveEnd(
"HTTP/1.1 101 Switching Protocols",
"",
"HTTP/1.1 204 No Content",
"",
"HTTP/1.1 205 Reset Content",
"",
"HTTP/1.1 304 Not Modified",
"",
"HTTP/1.1 200 OK",
"Content-Length: 0",
"",
"");
}
}
}
[Fact] [Fact]
public async Task ThrowingResultsIn500Response() public async Task ThrowingResultsIn500Response()
{ {