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;
|
private RequestProcessingStatus _requestProcessingStatus;
|
||||||
protected bool _keepAlive;
|
protected bool _keepAlive;
|
||||||
|
private bool _canHaveBody;
|
||||||
private bool _autoChunk;
|
private bool _autoChunk;
|
||||||
protected Exception _applicationException;
|
protected Exception _applicationException;
|
||||||
|
|
||||||
|
|
@ -171,7 +172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
{
|
{
|
||||||
if (HasResponseStarted)
|
if (HasResponseStarted)
|
||||||
{
|
{
|
||||||
ThrowResponseAlreadyStartedException(nameof(StatusCode));
|
BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
_statusCode = value;
|
_statusCode = value;
|
||||||
|
|
@ -189,7 +190,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
{
|
{
|
||||||
if (HasResponseStarted)
|
if (HasResponseStarted)
|
||||||
{
|
{
|
||||||
ThrowResponseAlreadyStartedException(nameof(ReasonPhrase));
|
BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.ReasonPhrase);
|
||||||
}
|
}
|
||||||
|
|
||||||
_reasonPhrase = value;
|
_reasonPhrase = value;
|
||||||
|
|
@ -425,7 +426,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
{
|
{
|
||||||
if (HasResponseStarted)
|
if (HasResponseStarted)
|
||||||
{
|
{
|
||||||
ThrowResponseAlreadyStartedException(nameof(OnStarting));
|
BadHttpResponse.ThrowException(ResponseRejectionReasons.OnStartingCannotBeSetResponseStarted, ResponseRejectionParameter.OnStarting);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_onStarting == null)
|
if (_onStarting == null)
|
||||||
|
|
@ -512,17 +513,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
{
|
{
|
||||||
ProduceStartAndFireOnStarting().GetAwaiter().GetResult();
|
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
|
else
|
||||||
{
|
{
|
||||||
SocketOutput.Write(data);
|
HandleNonBodyResponseWrite(data.Count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -533,17 +541,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
return WriteAsyncAwaited(data, cancellationToken);
|
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
|
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();
|
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
|
else
|
||||||
{
|
{
|
||||||
await SocketOutput.WriteAsync(data, cancellationToken: cancellationToken);
|
HandleNonBodyResponseWrite(data.Count);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteChunked(ArraySegment<byte> data)
|
private void WriteChunked(ArraySegment<byte> data)
|
||||||
|
|
@ -679,21 +704,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
if (!_requestRejected)
|
if (!_requestRejected)
|
||||||
{
|
{
|
||||||
// 500 Internal Server Error
|
// 500 Internal Server Error
|
||||||
StatusCode = 500;
|
ErrorResetHeadersToDefaults(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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -747,10 +758,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
bool appCompleted)
|
bool appCompleted)
|
||||||
{
|
{
|
||||||
var responseHeaders = FrameResponseHeaders;
|
var responseHeaders = FrameResponseHeaders;
|
||||||
responseHeaders.SetReadOnly();
|
|
||||||
|
|
||||||
var hasConnection = responseHeaders.HasConnection;
|
var hasConnection = responseHeaders.HasConnection;
|
||||||
|
|
||||||
|
// Set whether response can have body
|
||||||
|
_canHaveBody = StatusCanHaveBody(StatusCode) && Method != "HEAD";
|
||||||
|
|
||||||
var end = SocketOutput.ProducingStart();
|
var end = SocketOutput.ProducingStart();
|
||||||
if (_keepAlive && hasConnection)
|
if (_keepAlive && hasConnection)
|
||||||
{
|
{
|
||||||
|
|
@ -758,39 +771,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
_keepAlive = connectionValue.Equals("keep-alive", StringComparison.OrdinalIgnoreCase);
|
_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
|
if (appCompleted)
|
||||||
// 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
|
// Since the app has completed and we are only now generating
|
||||||
// the headers we can safely set the Content-Length to 0.
|
// the headers we can safely set the Content-Length to 0.
|
||||||
responseHeaders.SetRawContentLength("0", _bytesContentLengthZero);
|
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
|
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)
|
if (!_keepAlive && !hasConnection)
|
||||||
{
|
{
|
||||||
|
|
@ -1255,12 +1277,67 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
statusCode != 304;
|
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()
|
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(
|
throw new ObjectDisposedException(
|
||||||
"The response has been aborted due to an unhandled application exception.",
|
"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)
|
public void SetBadRequestState(BadHttpRequestException ex)
|
||||||
{
|
{
|
||||||
// Setting status code will throw if response has already started
|
ErrorResetHeadersToDefaults(statusCode: ex.StatusCode);
|
||||||
if (!HasResponseStarted)
|
|
||||||
{
|
|
||||||
StatusCode = ex.StatusCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
_keepAlive = false;
|
_keepAlive = false;
|
||||||
_requestProcessingStopping = true;
|
_requestProcessingStopping = true;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
{
|
{
|
||||||
if (_isReadOnly)
|
if (_isReadOnly)
|
||||||
{
|
{
|
||||||
ThrowHeadersReadOnlyException();
|
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted);
|
||||||
}
|
}
|
||||||
SetValueFast(key, value);
|
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()
|
protected void ThrowArgumentException()
|
||||||
{
|
{
|
||||||
throw new ArgumentException();
|
throw new ArgumentException();
|
||||||
|
|
@ -144,7 +139,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
{
|
{
|
||||||
if (_isReadOnly)
|
if (_isReadOnly)
|
||||||
{
|
{
|
||||||
ThrowHeadersReadOnlyException();
|
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted);
|
||||||
}
|
}
|
||||||
AddValueFast(key, value);
|
AddValueFast(key, value);
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +148,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
{
|
{
|
||||||
if (_isReadOnly)
|
if (_isReadOnly)
|
||||||
{
|
{
|
||||||
ThrowHeadersReadOnlyException();
|
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted);
|
||||||
}
|
}
|
||||||
ClearFast();
|
ClearFast();
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +195,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
||||||
{
|
{
|
||||||
if (_isReadOnly)
|
if (_isReadOnly)
|
||||||
{
|
{
|
||||||
ThrowHeadersReadOnlyException();
|
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted);
|
||||||
}
|
}
|
||||||
return RemoveFast(key);
|
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 ConnectionDisconnectedWrite(string connectionId, int count, Exception ex);
|
||||||
|
|
||||||
|
void ConnectionHeadResponseBodyWrite(string connectionId, int count);
|
||||||
|
|
||||||
void ConnectionBadRequest(string connectionId, BadHttpRequestException ex);
|
void ConnectionBadRequest(string connectionId, BadHttpRequestException ex);
|
||||||
|
|
||||||
void NotAllConnectionsClosedGracefully();
|
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> _applicationError;
|
||||||
private static readonly Action<ILogger, string, Exception> _connectionError;
|
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> _connectionDisconnectedWrite;
|
||||||
|
private static readonly Action<ILogger, string, int, Exception> _connectionHeadResponseBodyWrite;
|
||||||
private static readonly Action<ILogger, Exception> _notAllConnectionsClosedGracefully;
|
private static readonly Action<ILogger, Exception> _notAllConnectionsClosedGracefully;
|
||||||
private static readonly Action<ILogger, string, string, Exception> _connectionBadRequest;
|
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.");
|
_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.");
|
_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}""");
|
_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)
|
public KestrelTrace(ILogger logger)
|
||||||
|
|
@ -133,6 +135,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
|
||||||
_connectionDisconnectedWrite(_logger, connectionId, count, ex);
|
_connectionDisconnectedWrite(_logger, connectionId, count, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual void ConnectionHeadResponseBodyWrite(string connectionId, int count)
|
||||||
|
{
|
||||||
|
_connectionHeadResponseBodyWrite(_logger, connectionId, count, null);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual void NotAllConnectionsClosedGracefully()
|
public virtual void NotAllConnectionsClosedGracefully()
|
||||||
{
|
{
|
||||||
_notAllConnectionsClosedGracefully(_logger, null);
|
_notAllConnectionsClosedGracefully(_logger, null);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel;
|
using Microsoft.AspNetCore.Server.Kestrel;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Internal;
|
using Microsoft.AspNetCore.Server.Kestrel.Internal;
|
||||||
|
|
@ -1260,5 +1261,228 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
requestProcessingTask.Wait();
|
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