From e7e6b896ba5e5c201b61f70fa4ea352027bd6bd8 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Sat, 9 Jul 2016 08:48:36 +0100 Subject: [PATCH] Don't emit TE header or body for non-body responses --- .../BadHttpResponse.cs | 75 ++++++ .../Internal/Http/Frame.cs | 199 +++++++++++----- .../Internal/Http/FrameHeaders.cs | 13 +- .../Internal/Http/ResponseRejectionReasons.cs | 22 ++ .../Internal/Infrastructure/IKestrelTrace.cs | 2 + .../Internal/Infrastructure/KestrelTrace.cs | 7 + .../FrameTests.cs | 224 ++++++++++++++++++ .../TestFrameProtectedMembers.cs | 23 ++ 8 files changed, 493 insertions(+), 72 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Server.Kestrel/BadHttpResponse.cs create mode 100644 src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ResponseRejectionReasons.cs create mode 100644 test/Microsoft.AspNetCore.Server.KestrelTests/TestFrameProtectedMembers.cs diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpResponse.cs b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpResponse.cs new file mode 100644 index 0000000000..2e953b46ca --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpResponse.cs @@ -0,0 +1,75 @@ +// 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index 7b0c22dbd9..0ab83b51ea 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -60,6 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private RequestProcessingStatus _requestProcessingStatus; protected bool _keepAlive; + private bool _canHaveBody; private bool _autoChunk; protected Exception _applicationException; @@ -171,7 +172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { if (HasResponseStarted) { - ThrowResponseAlreadyStartedException(nameof(StatusCode)); + BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.StatusCode); } _statusCode = value; @@ -189,7 +190,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { if (HasResponseStarted) { - ThrowResponseAlreadyStartedException(nameof(ReasonPhrase)); + BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.ReasonPhrase); } _reasonPhrase = value; @@ -425,7 +426,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { if (HasResponseStarted) { - ThrowResponseAlreadyStartedException(nameof(OnStarting)); + BadHttpResponse.ThrowException(ResponseRejectionReasons.OnStartingCannotBeSetResponseStarted, ResponseRejectionParameter.OnStarting); } if (_onStarting == null) @@ -512,17 +513,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { ProduceStartAndFireOnStarting().GetAwaiter().GetResult(); - if (_autoChunk) + if (_canHaveBody) { - if (data.Count == 0) + if (_autoChunk) { - return; + if (data.Count == 0) + { + return; + } + WriteChunked(data); + } + else + { + SocketOutput.Write(data); } - WriteChunked(data); } else { - SocketOutput.Write(data); + HandleNonBodyResponseWrite(data.Count); } } @@ -533,17 +541,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http return WriteAsyncAwaited(data, cancellationToken); } - if (_autoChunk) + if (_canHaveBody) { - if (data.Count == 0) + if (_autoChunk) { - return TaskCache.CompletedTask; + if (data.Count == 0) + { + return TaskCache.CompletedTask; + } + return WriteChunkedAsync(data, cancellationToken); + } + else + { + return SocketOutput.WriteAsync(data, cancellationToken: cancellationToken); } - return WriteChunkedAsync(data, cancellationToken); } else { - return SocketOutput.WriteAsync(data, cancellationToken: cancellationToken); + HandleNonBodyResponseWrite(data.Count); + return TaskCache.CompletedTask; } } @@ -551,18 +567,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { await ProduceStartAndFireOnStarting(); - if (_autoChunk) + if (_canHaveBody) { - if (data.Count == 0) + if (_autoChunk) { - return; + if (data.Count == 0) + { + return; + } + await WriteChunkedAsync(data, cancellationToken); + } + else + { + await SocketOutput.WriteAsync(data, cancellationToken: cancellationToken); } - await WriteChunkedAsync(data, cancellationToken); } else { - await SocketOutput.WriteAsync(data, cancellationToken: cancellationToken); + HandleNonBodyResponseWrite(data.Count); + return; } + } private void WriteChunked(ArraySegment data) @@ -679,21 +704,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http if (!_requestRejected) { // 500 Internal Server Error - StatusCode = 500; - } - - ReasonPhrase = null; - - var responseHeaders = FrameResponseHeaders; - responseHeaders.Reset(); - var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues(); - - responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes); - responseHeaders.SetRawContentLength("0", _bytesContentLengthZero); - - if (ServerOptions.AddServerHeader) - { - responseHeaders.SetRawServer(Constants.ServerName, _bytesServer); + ErrorResetHeadersToDefaults(statusCode: 500); } } @@ -747,50 +758,61 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http bool appCompleted) { var responseHeaders = FrameResponseHeaders; - responseHeaders.SetReadOnly(); var hasConnection = responseHeaders.HasConnection; + // Set whether response can have body + _canHaveBody = StatusCanHaveBody(StatusCode) && Method != "HEAD"; + var end = SocketOutput.ProducingStart(); if (_keepAlive && hasConnection) { var connectionValue = responseHeaders.HeaderConnection.ToString(); _keepAlive = connectionValue.Equals("keep-alive", StringComparison.OrdinalIgnoreCase); } - - if (!responseHeaders.HasTransferEncoding && !responseHeaders.HasContentLength) + + if (_canHaveBody) { - if (appCompleted) + if (!responseHeaders.HasTransferEncoding && !responseHeaders.HasContentLength) { - // 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)) + if (appCompleted) { // Since the app has completed and we are only now generating // the headers we can safely set the Content-Length to 0. responseHeaders.SetRawContentLength("0", _bytesContentLengthZero); } - } - else if (_keepAlive) - { - // 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). - if (_httpVersion == Http.HttpVersion.Http11) - { - _autoChunk = true; - responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked); - } else { - _keepAlive = false; + // 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). + if (_httpVersion == Http.HttpVersion.Http11) + { + _autoChunk = true; + responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked); + } + else + { + _keepAlive = false; + } } } } + else + { + // Don't set the Content-Length or Transfer-Encoding headers + // automatically for HEAD requests or 101, 204, 205, 304 responses. + if (responseHeaders.HasTransferEncoding) + { + RejectNonBodyTransferEncodingResponse(appCompleted); + } + } + + responseHeaders.SetReadOnly(); if (!_keepAlive && !hasConnection) { @@ -1255,12 +1277,67 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http statusCode != 304; } - private void ThrowResponseAlreadyStartedException(string value) + private void RejectNonBodyTransferEncodingResponse(bool appCompleted) { - throw new InvalidOperationException(value + " cannot be set, response had already started."); + var ex = BadHttpResponse.GetException(ResponseRejectionReasons.TransferEncodingSetOnNonBodyResponse, StatusCode); + if (!appCompleted) + { + // Back out of header creation surface exeception in user code + _requestProcessingStatus = RequestProcessingStatus.RequestStarted; + throw ex; + } + else + { + ReportApplicationError(ex); + // 500 Internal Server Error + ErrorResetHeadersToDefaults(statusCode: 500); + } + } + + private void ErrorResetHeadersToDefaults(int statusCode) + { + // Setting status code will throw if response has already started + if (!HasResponseStarted) + { + StatusCode = statusCode; + ReasonPhrase = null; + } + + var responseHeaders = FrameResponseHeaders; + responseHeaders.Reset(); + var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues(); + + responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes); + responseHeaders.SetRawContentLength("0", _bytesContentLengthZero); + + if (ServerOptions.AddServerHeader) + { + responseHeaders.SetRawServer(Constants.ServerName, _bytesServer); + } + } + + public void HandleNonBodyResponseWrite(int count) + { + if (Method == "HEAD") + { + // Don't write to body for HEAD requests. + Log.ConnectionHeadResponseBodyWrite(ConnectionId, count); + } + else + { + // Throw Exception for 101, 204, 205, 304 responses. + BadHttpResponse.ThrowException(ResponseRejectionReasons.WriteToNonBodyResponse, StatusCode); + } } private void ThrowResponseAbortedException() + { + throw new ObjectDisposedException( + "The response has been aborted due to an unhandled application exception.", + _applicationException); + } + + public void RejectRequest(string message) { throw new ObjectDisposedException( "The response has been aborted due to an unhandled application exception.", @@ -1288,11 +1365,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http public void SetBadRequestState(BadHttpRequestException ex) { - // Setting status code will throw if response has already started - if (!HasResponseStarted) - { - StatusCode = ex.StatusCode; - } + ErrorResetHeadersToDefaults(statusCode: ex.StatusCode); _keepAlive = false; _requestProcessingStopping = true; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs index ac283a006c..d91aec8d6d 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { if (_isReadOnly) { - ThrowHeadersReadOnlyException(); + BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); } SetValueFast(key, value); } @@ -48,11 +48,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http } } - protected void ThrowHeadersReadOnlyException() - { - throw new InvalidOperationException("Headers are read-only, response has already started."); - } - protected void ThrowArgumentException() { throw new ArgumentException(); @@ -144,7 +139,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { if (_isReadOnly) { - ThrowHeadersReadOnlyException(); + BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); } AddValueFast(key, value); } @@ -153,7 +148,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { if (_isReadOnly) { - ThrowHeadersReadOnlyException(); + BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); } ClearFast(); } @@ -200,7 +195,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { if (_isReadOnly) { - ThrowHeadersReadOnlyException(); + BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); } return RemoveFast(key); } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ResponseRejectionReasons.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ResponseRejectionReasons.cs new file mode 100644 index 0000000000..4ccc699020 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ResponseRejectionReasons.cs @@ -0,0 +1,22 @@ +// 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 + } + +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/IKestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/IKestrelTrace.cs index e18a99ce21..900adc8766 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/IKestrelTrace.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/IKestrelTrace.cs @@ -33,6 +33,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure void ConnectionDisconnectedWrite(string connectionId, int count, Exception ex); + void ConnectionHeadResponseBodyWrite(string connectionId, int count); + void ConnectionBadRequest(string connectionId, BadHttpRequestException ex); void NotAllConnectionsClosedGracefully(); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelTrace.cs index 0215b45374..5d3ecff7bd 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelTrace.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelTrace.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal private static readonly Action _applicationError; private static readonly Action _connectionError; private static readonly Action _connectionDisconnectedWrite; + private static readonly Action _connectionHeadResponseBodyWrite; private static readonly Action _notAllConnectionsClosedGracefully; private static readonly Action _connectionBadRequest; @@ -48,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal _connectionDisconnectedWrite = LoggerMessage.Define(LogLevel.Debug, 15, @"Connection id ""{ConnectionId}"" write of ""{count}"" bytes to disconnected client."); _notAllConnectionsClosedGracefully = LoggerMessage.Define(LogLevel.Debug, 16, "Some connections failed to close gracefully during server shutdown."); _connectionBadRequest = LoggerMessage.Define(LogLevel.Information, 17, @"Connection id ""{ConnectionId}"" bad request data: ""{message}"""); + _connectionHeadResponseBodyWrite = LoggerMessage.Define(LogLevel.Debug, 18, @"Connection id ""{ConnectionId}"" write of ""{count}"" body bytes to non-body HEAD response."); } public KestrelTrace(ILogger logger) @@ -133,6 +135,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal _connectionDisconnectedWrite(_logger, connectionId, count, ex); } + public virtual void ConnectionHeadResponseBodyWrite(string connectionId, int count) + { + _connectionHeadResponseBodyWrite(_logger, connectionId, count, null); + } + public virtual void NotAllConnectionsClosedGracefully() { _notAllConnectionsClosedGracefully(_logger, null); diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs index c59e74160b..86962eccd5 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Text; +using System.Threading; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel.Internal; @@ -1260,5 +1261,228 @@ namespace Microsoft.AspNetCore.Server.KestrelTests requestProcessingTask.Wait(); } } + + [Fact] + public void FlushSetsTransferEncodingSetForUnknownLengthBodyResponse() + { + // 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(application: null, context: connectionContext); + 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(application: null, context: connectionContext); + frame.InitializeHeaders(); + frame.KeepAlive = true; + frame.HttpVersion = "HTTP/1.1"; + ((IHttpResponseFeature)frame).StatusCode = 304; + + // Act + 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(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(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(() => frame.Write(new ArraySegment(new byte[1]))); + Assert.ThrowsAsync(() => frame.WriteAsync(new ArraySegment(new byte[1]), default(CancellationToken))); + + frame.Flush(); // Does not throw + } + + [Fact] + public void WriteDoesNotThrowForHeadResponse() + { + // 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(application: null, context: connectionContext); + frame.InitializeHeaders(); + frame.KeepAlive = true; + frame.HttpVersion = "HTTP/1.1"; + ((IHttpRequestFeature)frame).Method = "HEAD"; + + // Assert + frame.Flush(); // Does not throw + + frame.Write(new ArraySegment(new byte[1])); + + frame.Flush(); // Does not throw + } + + + [Fact] + public void ManuallySettingTransferEncodingThrowsForHeadResponse() + { + // 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(application: null, context: connectionContext); + frame.InitializeHeaders(); + frame.KeepAlive = true; + frame.HttpVersion = "HTTP/1.1"; + ((IHttpRequestFeature)frame).Method = "HEAD"; + + //Act + frame.ResponseHeaders.Add("Transfer-Encoding", "chunked"); + + // Assert + Assert.Throws(() => frame.Flush()); + } + + [Fact] + public void ManuallySettingTransferEncodingThrowsForNoBodyResponse() + { + // 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(application: null, context: connectionContext); + frame.InitializeHeaders(); + frame.KeepAlive = true; + frame.HttpVersion = "HTTP/1.1"; + ((IHttpResponseFeature)frame).StatusCode = 304; + + //Act + frame.ResponseHeaders.Add("Transfer-Encoding", "chunked"); + + // Assert + Assert.Throws(() => frame.Flush()); + } } } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestFrameProtectedMembers.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestFrameProtectedMembers.cs new file mode 100644 index 0000000000..01d6418cec --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestFrameProtectedMembers.cs @@ -0,0 +1,23 @@ +// 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 : Frame + { + public TestFrameProtectedMembers(IHttpApplication application, ConnectionContext context) + : base(application, context) + { + } + + public bool KeepAlive + { + get { return _keepAlive; } + set { _keepAlive = value; } + } + } +}