Skip to content
This repository has been archived by the owner on Dec 18, 2018. It is now read-only.

Set Content-Length: 0 when an AppFunc completes without a write #174

Merged
merged 2 commits into from
Aug 26, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 41 additions & 13 deletions src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,6 @@ private async Task ExecuteAsync()
{
FireOnStarting();
}

if (_autoChunk)
{
WriteChunkedResponseSuffix();
}
}
catch (Exception ex)
{
Expand Down Expand Up @@ -380,7 +375,7 @@ public void ProduceContinue()
}
}

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.
if (_responseStarted) return;
Expand All @@ -389,7 +384,7 @@ public void ProduceStart(bool immediate = true)

var status = ReasonPhrases.ToStatus(StatusCode, ReasonPhrase);

var responseHeader = CreateResponseHeader(status, ResponseHeaders);
var responseHeader = CreateResponseHeader(status, appCompleted, ResponseHeaders);
SocketOutput.Write(
responseHeader.Item1,
(error, x) =>
Expand Down Expand Up @@ -428,7 +423,14 @@ public void ProduceEnd(Exception ex)
}
}

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)
{
Expand All @@ -440,7 +442,9 @@ public void ProduceEnd(Exception ex)
}

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);
writer.Write(HttpVersion);
Expand Down Expand Up @@ -492,16 +496,31 @@ public void ProduceEnd(Exception ex)

if (_keepAlive && !hasTransferEncoding && !hasContentLength)
{
if (HttpVersion == "HTTP/1.1")
if (appCompleted)
{
_autoChunk = true;
writer.Write("Transfer-Encoding: chunked\r\n");
// Don't set the Content-Length or Transfer-Encoding headers
// 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
{
_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")
{
writer.Write("Connection: close\r\n\r\n");
Expand Down Expand Up @@ -661,5 +680,14 @@ private void AddRequestHeader(byte[] keyBytes, int keyOffset, int keyLength, str
{
_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;
}
}
}
81 changes: 81 additions & 0 deletions test/Microsoft.AspNet.Server.KestrelTests/ChunkedResponseTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Text;
using System.Threading.Tasks;
using Xunit;
Expand Down Expand Up @@ -71,5 +72,85 @@ public async Task ZeroLengthWritesAreIgnored()
}
}
}

[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();
}
}
}
}
}

164 changes: 164 additions & 0 deletions test/Microsoft.AspNet.Server.KestrelTests/EngineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ private async Task AppChunked(Frame frame)
await frame.ResponseBody.WriteAsync(bytes, 0, bytes.Length);
}

private Task EmptyApp(Frame frame)
{
frame.ResponseHeaders.Clear();
return Task.FromResult<object>(null);
}

[Fact]
public void EngineCanStartAndStop()
{
Expand Down Expand Up @@ -235,6 +241,35 @@ public async Task Http10KeepAlive()
}
}

[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]
public async Task Http10KeepAliveContentLength()
{
Expand Down Expand Up @@ -346,6 +381,135 @@ public async Task DisconnectingClient()
}
}

[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]
public async Task ThrowingResultsIn500Response()
{
Expand Down