diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs index 43bed0797..d5dc95808 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs @@ -254,11 +254,6 @@ private async Task ExecuteAsync() { FireOnStarting(); } - - if (_autoChunk) - { - WriteChunkedResponseSuffix(); - } } catch (Exception ex) { @@ -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; @@ -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) => @@ -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) { @@ -440,7 +442,9 @@ public void ProduceEnd(Exception ex) } private Tuple, IDisposable> CreateResponseHeader( - string status, IEnumerable> headers) + string status, + bool appCompleted, + IEnumerable> headers) { var writer = new MemoryPoolTextWriter(Memory); writer.Write(HttpVersion); @@ -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"); @@ -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; + } } } diff --git a/test/Microsoft.AspNet.Server.KestrelTests/ChunkedResponseTests.cs b/test/Microsoft.AspNet.Server.KestrelTests/ChunkedResponseTests.cs index a1b7ac186..162a29263 100644 --- a/test/Microsoft.AspNet.Server.KestrelTests/ChunkedResponseTests.cs +++ b/test/Microsoft.AspNet.Server.KestrelTests/ChunkedResponseTests.cs @@ -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; @@ -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(); + } + } + } } } + diff --git a/test/Microsoft.AspNet.Server.KestrelTests/EngineTests.cs b/test/Microsoft.AspNet.Server.KestrelTests/EngineTests.cs index 27526edd8..a293e8bae 100644 --- a/test/Microsoft.AspNet.Server.KestrelTests/EngineTests.cs +++ b/test/Microsoft.AspNet.Server.KestrelTests/EngineTests.cs @@ -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(null); + } + [Fact] public void EngineCanStartAndStop() { @@ -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() { @@ -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() {