Don't emit TE header or body for non-body responses
This commit is contained in:
parent
e8fa40235b
commit
e7e6b896ba
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte> 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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
|
|||
private static readonly Action<ILogger, string, Exception> _applicationError;
|
||||
private static readonly Action<ILogger, string, Exception> _connectionError;
|
||||
private static readonly Action<ILogger, string, int, Exception> _connectionDisconnectedWrite;
|
||||
private static readonly Action<ILogger, string, int, Exception> _connectionHeadResponseBodyWrite;
|
||||
private static readonly Action<ILogger, Exception> _notAllConnectionsClosedGracefully;
|
||||
private static readonly Action<ILogger, string, string, Exception> _connectionBadRequest;
|
||||
|
||||
|
|
@ -48,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
|
|||
_connectionDisconnectedWrite = LoggerMessage.Define<string, int>(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<string, string>(LogLevel.Information, 17, @"Connection id ""{ConnectionId}"" bad request data: ""{message}""");
|
||||
_connectionHeadResponseBodyWrite = LoggerMessage.Define<string, int>(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);
|
||||
|
|
|
|||
|
|
@ -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<object>(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<object>(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<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.ThrowsAsync<InvalidOperationException>(() => frame.WriteAsync(new ArraySegment<byte>(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<object>(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<byte>(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<object>(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<InvalidOperationException>(() => 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<object>(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<InvalidOperationException>(() => frame.Flush());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TContext> : Frame<TContext>
|
||||
{
|
||||
public TestFrameProtectedMembers(IHttpApplication<TContext> application, ConnectionContext context)
|
||||
: base(application, context)
|
||||
{
|
||||
}
|
||||
|
||||
public bool KeepAlive
|
||||
{
|
||||
get { return _keepAlive; }
|
||||
set { _keepAlive = value; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue