Handle response content length mismatches (#175).
This commit is contained in:
parent
8c103f0f23
commit
f8813a600d
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
|
@ -75,7 +76,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
protected readonly long _keepAliveMilliseconds;
|
||||
private readonly long _requestHeadersTimeoutMilliseconds;
|
||||
|
||||
private int _responseBytesWritten;
|
||||
protected long _responseBytesWritten;
|
||||
|
||||
public Frame(ConnectionContext context)
|
||||
{
|
||||
|
|
@ -516,8 +517,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
|
||||
public void Write(ArraySegment<byte> data)
|
||||
{
|
||||
VerifyAndUpdateWrite(data.Count);
|
||||
ProduceStartAndFireOnStarting().GetAwaiter().GetResult();
|
||||
_responseBytesWritten += data.Count;
|
||||
|
||||
if (_canHaveBody)
|
||||
{
|
||||
|
|
@ -547,7 +548,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
return WriteAsyncAwaited(data, cancellationToken);
|
||||
}
|
||||
|
||||
_responseBytesWritten += data.Count;
|
||||
VerifyAndUpdateWrite(data.Count);
|
||||
|
||||
if (_canHaveBody)
|
||||
{
|
||||
|
|
@ -573,8 +574,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
|
||||
public async Task WriteAsyncAwaited(ArraySegment<byte> data, CancellationToken cancellationToken)
|
||||
{
|
||||
VerifyAndUpdateWrite(data.Count);
|
||||
|
||||
await ProduceStartAndFireOnStarting();
|
||||
_responseBytesWritten += data.Count;
|
||||
|
||||
if (_canHaveBody)
|
||||
{
|
||||
|
|
@ -598,6 +600,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
private void VerifyAndUpdateWrite(int count)
|
||||
{
|
||||
var responseHeaders = FrameResponseHeaders;
|
||||
|
||||
if (responseHeaders != null &&
|
||||
!responseHeaders.HasTransferEncoding &&
|
||||
responseHeaders.HasContentLength &&
|
||||
_responseBytesWritten + count > responseHeaders.HeaderContentLengthValue.Value)
|
||||
{
|
||||
_keepAlive = false;
|
||||
throw new InvalidOperationException(
|
||||
$"Response Content-Length mismatch: too many bytes written ({_responseBytesWritten + count} of {responseHeaders.HeaderContentLengthValue.Value}).");
|
||||
}
|
||||
|
||||
_responseBytesWritten += count;
|
||||
}
|
||||
|
||||
private void WriteChunked(ArraySegment<byte> data)
|
||||
{
|
||||
SocketOutput.Write(data, chunk: true);
|
||||
|
|
|
|||
|
|
@ -3697,6 +3697,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
{
|
||||
_bits = 0;
|
||||
_headers = default(HeaderReferences);
|
||||
|
||||
MaybeUnknown?.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -5670,6 +5671,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
}
|
||||
set
|
||||
{
|
||||
_contentLength = ParseContentLength(value);
|
||||
_bits |= 2048L;
|
||||
_headers._ContentLength = value;
|
||||
_headers._rawContentLength = null;
|
||||
|
|
@ -7384,6 +7386,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
{
|
||||
if ("Content-Length".Equals(key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_contentLength = ParseContentLength(value);
|
||||
_bits |= 2048L;
|
||||
_headers._ContentLength = value;
|
||||
_headers._rawContentLength = null;
|
||||
|
|
@ -7809,6 +7812,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
{
|
||||
ThrowDuplicateKeyException();
|
||||
}
|
||||
_contentLength = ParseContentLength(value);
|
||||
_bits |= 2048L;
|
||||
_headers._ContentLength = value;
|
||||
_headers._rawContentLength = null;
|
||||
|
|
@ -8350,6 +8354,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
{
|
||||
if (((_bits & 2048L) != 0))
|
||||
{
|
||||
_contentLength = null;
|
||||
_bits &= ~2048L;
|
||||
_headers._ContentLength = StringValues.Empty;
|
||||
_headers._rawContentLength = null;
|
||||
|
|
@ -8601,6 +8606,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
{
|
||||
_bits = 0;
|
||||
_headers = default(HeaderReferences);
|
||||
_contentLength = null;
|
||||
MaybeUnknown?.Clear();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
|
@ -232,6 +233,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
public static long ParseContentLength(StringValues value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return long.Parse(value, NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new InvalidOperationException("Content-Length value must be an integral number.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ThrowInvalidHeaderCharacter(char ch)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format("Invalid non-ASCII or control character in header: 0x{0:X4}", (ushort)ch));
|
||||
|
|
|
|||
|
|
@ -92,6 +92,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
try
|
||||
{
|
||||
await _application.ProcessRequestAsync(context).ConfigureAwait(false);
|
||||
|
||||
var responseHeaders = FrameResponseHeaders;
|
||||
if (!responseHeaders.HasTransferEncoding &&
|
||||
responseHeaders.HasContentLength &&
|
||||
_responseBytesWritten < responseHeaders.HeaderContentLengthValue.Value)
|
||||
{
|
||||
_keepAlive = false;
|
||||
ReportApplicationError(new InvalidOperationException(
|
||||
$"Response Content-Length mismatch: too few bytes written ({_responseBytesWritten} of {responseHeaders.HeaderContentLengthValue.Value})."));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
private static readonly byte[] _CrLf = new[] { (byte)'\r', (byte)'\n' };
|
||||
private static readonly byte[] _colonSpace = new[] { (byte)':', (byte)' ' };
|
||||
|
||||
private long? _contentLength;
|
||||
|
||||
public bool HasConnection => HeaderConnection.Count != 0;
|
||||
|
||||
public bool HasTransferEncoding => HeaderTransferEncoding.Count != 0;
|
||||
|
|
@ -23,6 +25,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
|
||||
public bool HasDate => HeaderDate.Count != 0;
|
||||
|
||||
public long? HeaderContentLengthValue => _contentLength;
|
||||
|
||||
public Enumerator GetEnumerator()
|
||||
{
|
||||
return new Enumerator(this);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
|
|||
|
||||
void ConnectionDisconnectedWrite(string connectionId, int count, Exception ex);
|
||||
|
||||
void ConnectionHeadResponseBodyWrite(string connectionId, int count);
|
||||
void ConnectionHeadResponseBodyWrite(string connectionId, long count);
|
||||
|
||||
void ConnectionBadRequest(string connectionId, BadHttpRequestException ex);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +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, string, long, Exception> _connectionHeadResponseBodyWrite;
|
||||
private static readonly Action<ILogger, Exception> _notAllConnectionsClosedGracefully;
|
||||
private static readonly Action<ILogger, string, string, Exception> _connectionBadRequest;
|
||||
|
||||
|
|
@ -49,7 +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.");
|
||||
_connectionHeadResponseBodyWrite = LoggerMessage.Define<string, long>(LogLevel.Debug, 18, @"Connection id ""{ConnectionId}"" write of ""{count}"" body bytes to non-body HEAD response.");
|
||||
}
|
||||
|
||||
public KestrelTrace(ILogger logger)
|
||||
|
|
@ -135,7 +135,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
|
|||
_connectionDisconnectedWrite(_logger, connectionId, count, ex);
|
||||
}
|
||||
|
||||
public virtual void ConnectionHeadResponseBodyWrite(string connectionId, int count)
|
||||
public virtual void ConnectionHeadResponseBodyWrite(string connectionId, long count)
|
||||
{
|
||||
_connectionHeadResponseBodyWrite(_logger, connectionId, count, null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
|
@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
|||
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
|
@ -85,7 +87,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
app.Run(async context =>
|
||||
{
|
||||
context.Response.Headers.Add(headerName, headerValue);
|
||||
|
||||
|
||||
await context.Response.WriteAsync("");
|
||||
});
|
||||
});
|
||||
|
|
@ -299,7 +301,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResponseBodyNotWrittenOnHeadResponse()
|
||||
public async Task ResponseBodyNotWrittenOnHeadResponseAndLoggedOnlyOnce()
|
||||
{
|
||||
var mockKestrelTrace = new Mock<IKestrelTrace>();
|
||||
|
||||
|
|
@ -324,7 +326,285 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
}
|
||||
|
||||
mockKestrelTrace.Verify(kestrelTrace =>
|
||||
kestrelTrace.ConnectionHeadResponseBodyWrite(It.IsAny<string>(), "hello, world".Length));
|
||||
kestrelTrace.ConnectionHeadResponseBodyWrite(It.IsAny<string>(), "hello, world".Length), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenAppWritesMoreThanContentLengthWriteThrowsAndConnectionCloses()
|
||||
{
|
||||
var testLogger = new TestApplicationErrorLogger();
|
||||
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
|
||||
|
||||
using (var server = new TestServer(httpContext =>
|
||||
{
|
||||
httpContext.Response.ContentLength = 11;
|
||||
httpContext.Response.Body.Write(Encoding.ASCII.GetBytes("hello,"), 0, 6);
|
||||
httpContext.Response.Body.Write(Encoding.ASCII.GetBytes(" world"), 0, 6);
|
||||
return TaskCache.CompletedTask;
|
||||
}, serviceContext))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.Send(
|
||||
"GET / HTTP/1.1",
|
||||
"",
|
||||
"");
|
||||
await connection.ReceiveEnd(
|
||||
$"HTTP/1.1 200 OK",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"hello,");
|
||||
}
|
||||
}
|
||||
|
||||
var logMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error);
|
||||
Assert.Equal(
|
||||
$"Response Content-Length mismatch: too many bytes written (12 of 11).",
|
||||
logMessage.Exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenAppWritesMoreThanContentLengthWriteAsyncThrowsAndConnectionCloses()
|
||||
{
|
||||
var testLogger = new TestApplicationErrorLogger();
|
||||
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
|
||||
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
httpContext.Response.ContentLength = 11;
|
||||
await httpContext.Response.WriteAsync("hello,");
|
||||
await httpContext.Response.WriteAsync(" world");
|
||||
}, serviceContext))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.Send(
|
||||
"GET / HTTP/1.1",
|
||||
"",
|
||||
"");
|
||||
await connection.ReceiveEnd(
|
||||
$"HTTP/1.1 200 OK",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"hello,");
|
||||
}
|
||||
}
|
||||
|
||||
var logMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error);
|
||||
Assert.Equal(
|
||||
$"Response Content-Length mismatch: too many bytes written (12 of 11).",
|
||||
logMessage.Exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenAppWritesMoreThanContentLengthAndResponseNotStarted500ResponseSentAndConnectionCloses()
|
||||
{
|
||||
var testLogger = new TestApplicationErrorLogger();
|
||||
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
|
||||
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
httpContext.Response.ContentLength = 5;
|
||||
await httpContext.Response.WriteAsync("hello, world");
|
||||
}, serviceContext))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.Send(
|
||||
"GET / HTTP/1.1",
|
||||
"",
|
||||
"");
|
||||
await connection.ReceiveEnd(
|
||||
$"HTTP/1.1 500 Internal Server Error",
|
||||
"Connection: close",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"Content-Length: 0",
|
||||
"",
|
||||
"");
|
||||
}
|
||||
}
|
||||
|
||||
var logMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error);
|
||||
Assert.Equal(
|
||||
$"Response Content-Length mismatch: too many bytes written (12 of 5).",
|
||||
logMessage.Exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenAppWritesLessThanContentLengthErrorLogged()
|
||||
{
|
||||
var testLogger = new TestApplicationErrorLogger();
|
||||
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
|
||||
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
httpContext.Response.ContentLength = 13;
|
||||
await httpContext.Response.WriteAsync("hello, world");
|
||||
}, serviceContext))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.Send(
|
||||
"GET / HTTP/1.1",
|
||||
"",
|
||||
"");
|
||||
await connection.ReceiveEnd(
|
||||
$"HTTP/1.1 200 OK",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"Content-Length: 13",
|
||||
"",
|
||||
"hello, world");
|
||||
}
|
||||
}
|
||||
|
||||
var errorMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error);
|
||||
Assert.Equal(
|
||||
$"Response Content-Length mismatch: too few bytes written (12 of 13).",
|
||||
errorMessage.Exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenAppSetsContentLengthButDoesNotWriteBody500ResponseSentAndConnectionCloses()
|
||||
{
|
||||
var testLogger = new TestApplicationErrorLogger();
|
||||
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
|
||||
|
||||
using (var server = new TestServer(httpContext =>
|
||||
{
|
||||
httpContext.Response.ContentLength = 5;
|
||||
return TaskCache.CompletedTask;
|
||||
}, serviceContext))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.Send(
|
||||
"GET / HTTP/1.1",
|
||||
"",
|
||||
"");
|
||||
await connection.ReceiveEnd(
|
||||
$"HTTP/1.1 500 Internal Server Error",
|
||||
"Connection: close",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"Content-Length: 0",
|
||||
"",
|
||||
"");
|
||||
}
|
||||
}
|
||||
|
||||
var errorMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error);
|
||||
Assert.Equal(
|
||||
$"Response Content-Length mismatch: too few bytes written (0 of 5).",
|
||||
errorMessage.Exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task WhenAppSetsContentLengthToZeroAndDoesNotWriteNoErrorIsThrown(bool flushResponse)
|
||||
{
|
||||
var testLogger = new TestApplicationErrorLogger();
|
||||
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
|
||||
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
httpContext.Response.ContentLength = 0;
|
||||
|
||||
if (flushResponse)
|
||||
{
|
||||
await httpContext.Response.Body.FlushAsync();
|
||||
}
|
||||
}, serviceContext))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.Send(
|
||||
"GET / HTTP/1.1",
|
||||
"",
|
||||
"");
|
||||
await connection.Receive(
|
||||
$"HTTP/1.1 200 OK",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"Content-Length: 0",
|
||||
"",
|
||||
"");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(0, testLogger.ApplicationErrorsLogged);
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc7230#section-3.3.3
|
||||
// If a message is received with both a Transfer-Encoding and a
|
||||
// Content-Length header field, the Transfer-Encoding overrides the
|
||||
// Content-Length.
|
||||
[Fact]
|
||||
public async Task WhenAppSetsTransferEncodingAndContentLengthWritingLessIsNotAnError()
|
||||
{
|
||||
var testLogger = new TestApplicationErrorLogger();
|
||||
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
|
||||
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
httpContext.Response.Headers["Transfer-Encoding"] = "chunked";
|
||||
httpContext.Response.ContentLength = 13;
|
||||
await httpContext.Response.WriteAsync("hello, world");
|
||||
}, serviceContext))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.Send(
|
||||
"GET / HTTP/1.1",
|
||||
"",
|
||||
"");
|
||||
await connection.Receive(
|
||||
$"HTTP/1.1 200 OK",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"Transfer-Encoding: chunked",
|
||||
"Content-Length: 13",
|
||||
"",
|
||||
"hello, world");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(0, testLogger.ApplicationErrorsLogged);
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc7230#section-3.3.3
|
||||
// If a message is received with both a Transfer-Encoding and a
|
||||
// Content-Length header field, the Transfer-Encoding overrides the
|
||||
// Content-Length.
|
||||
[Fact]
|
||||
public async Task WhenAppSetsTransferEncodingAndContentLengthWritingMoreIsNotAnError()
|
||||
{
|
||||
var testLogger = new TestApplicationErrorLogger();
|
||||
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
|
||||
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
httpContext.Response.Headers["Transfer-Encoding"] = "chunked";
|
||||
httpContext.Response.ContentLength = 11;
|
||||
await httpContext.Response.WriteAsync("hello, world");
|
||||
}, serviceContext))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.Send(
|
||||
"GET / HTTP/1.1",
|
||||
"",
|
||||
"");
|
||||
await connection.Receive(
|
||||
$"HTTP/1.1 200 OK",
|
||||
$"Date: {server.Context.DateHeaderValue}",
|
||||
"Transfer-Encoding: chunked",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"hello, world");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(0, testLogger.ApplicationErrorsLogged);
|
||||
}
|
||||
|
||||
public static TheoryData<string, StringValues, string> NullHeaderData
|
||||
|
|
|
|||
|
|
@ -620,35 +620,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task WriteOnHeadResponseLoggedOnlyOnce(TestServiceContext testContext)
|
||||
{
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
await httpContext.Response.WriteAsync("hello, ");
|
||||
await httpContext.Response.WriteAsync("world");
|
||||
await httpContext.Response.WriteAsync("!");
|
||||
}, testContext))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.SendEnd(
|
||||
"HEAD / HTTP/1.1",
|
||||
"",
|
||||
"");
|
||||
await connection.ReceiveEnd(
|
||||
"HTTP/1.1 200 OK",
|
||||
$"Date: {testContext.DateHeaderValue}",
|
||||
"",
|
||||
"");
|
||||
}
|
||||
|
||||
Assert.Equal(1, ((TestKestrelTrace)testContext.Log).HeadResponseWrites);
|
||||
Assert.Equal(13, ((TestKestrelTrace)testContext.Log).HeadResponseWriteByteCount);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task ThrowingResultsIn500Response(TestServiceContext testContext)
|
||||
|
|
@ -697,11 +668,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
"Content-Length: 0",
|
||||
"",
|
||||
"");
|
||||
|
||||
Assert.False(onStartingCalled);
|
||||
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.False(onStartingCalled);
|
||||
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -739,11 +710,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello World");
|
||||
|
||||
Assert.True(onStartingCalled);
|
||||
Assert.Equal(1, testLogger.ApplicationErrorsLogged);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(onStartingCalled);
|
||||
Assert.Equal(1, testLogger.ApplicationErrorsLogged);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -781,11 +752,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello");
|
||||
|
||||
Assert.True(onStartingCalled);
|
||||
Assert.Equal(1, testLogger.ApplicationErrorsLogged);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(onStartingCalled);
|
||||
Assert.Equal(1, testLogger.ApplicationErrorsLogged);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -925,16 +896,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
"Content-Length: 0",
|
||||
"",
|
||||
"");
|
||||
|
||||
Assert.Equal(2, onStartingCallCount2);
|
||||
|
||||
// The first registered OnStarting callback should not be called,
|
||||
// since they are called LIFO and the other one failed.
|
||||
Assert.Equal(0, onStartingCallCount1);
|
||||
|
||||
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
|
||||
}
|
||||
}
|
||||
|
||||
// The first registered OnStarting callback should not be called,
|
||||
// since they are called LIFO and the other one failed.
|
||||
Assert.Equal(0, onStartingCallCount1);
|
||||
Assert.Equal(2, onStartingCallCount2);
|
||||
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -979,12 +948,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
"",
|
||||
"Hello World");
|
||||
}
|
||||
|
||||
// All OnCompleted callbacks should be called even if they throw.
|
||||
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
|
||||
Assert.True(onCompletedCalled1);
|
||||
Assert.True(onCompletedCalled2);
|
||||
}
|
||||
|
||||
// All OnCompleted callbacks should be called even if they throw.
|
||||
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
|
||||
Assert.True(onCompletedCalled1);
|
||||
Assert.True(onCompletedCalled2);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -78,24 +78,29 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
{
|
||||
var responseHeaders = new FrameResponseHeaders();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => {
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
((IHeaderDictionary)responseHeaders)[key] = value;
|
||||
});
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => {
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
((IHeaderDictionary)responseHeaders)[key] = new StringValues(new[] { "valid", value });
|
||||
});
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => {
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
((IDictionary<string, StringValues>)responseHeaders)[key] = value;
|
||||
});
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => {
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
var kvp = new KeyValuePair<string, StringValues>(key, value);
|
||||
((ICollection<KeyValuePair<string, StringValues>>)responseHeaders).Add(kvp);
|
||||
});
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => {
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
var kvp = new KeyValuePair<string, StringValues>(key, value);
|
||||
((IDictionary<string, StringValues>)responseHeaders).Add(key, value);
|
||||
});
|
||||
|
|
@ -142,5 +147,83 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
|
||||
Assert.Throws<InvalidOperationException>(() => dictionary.Clear());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsWhenAddingContentLengthWithNonNumericValue()
|
||||
{
|
||||
var headers = new FrameResponseHeaders();
|
||||
var dictionary = (IDictionary<string, StringValues>)headers;
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => dictionary.Add("Content-Length", new[] { "bad" }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsWhenSettingContentLengthToNonNumericValue()
|
||||
{
|
||||
var headers = new FrameResponseHeaders();
|
||||
var dictionary = (IDictionary<string, StringValues>)headers;
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => ((IHeaderDictionary)headers)["Content-Length"] = "bad");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsWhenAssigningHeaderContentLengthToNonNumericValue()
|
||||
{
|
||||
var headers = new FrameResponseHeaders();
|
||||
Assert.Throws<InvalidOperationException>(() => headers.HeaderContentLength = "bad");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContentLengthValueCanBeReadAsLongAfterAddingHeader()
|
||||
{
|
||||
var headers = new FrameResponseHeaders();
|
||||
var dictionary = (IDictionary<string, StringValues>)headers;
|
||||
dictionary.Add("Content-Length", "42");
|
||||
|
||||
Assert.Equal(42, headers.HeaderContentLengthValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContentLengthValueCanBeReadAsLongAfterSettingHeader()
|
||||
{
|
||||
var headers = new FrameResponseHeaders();
|
||||
var dictionary = (IDictionary<string, StringValues>)headers;
|
||||
dictionary["Content-Length"] = "42";
|
||||
|
||||
Assert.Equal(42, headers.HeaderContentLengthValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContentLengthValueCanBeReadAsLongAfterAssigningHeader()
|
||||
{
|
||||
var headers = new FrameResponseHeaders();
|
||||
headers.HeaderContentLength = "42";
|
||||
|
||||
Assert.Equal(42, headers.HeaderContentLengthValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContentLengthValueClearedWhenHeaderIsRemoved()
|
||||
{
|
||||
var headers = new FrameResponseHeaders();
|
||||
headers.HeaderContentLength = "42";
|
||||
var dictionary = (IDictionary<string, StringValues>)headers;
|
||||
|
||||
dictionary.Remove("Content-Length");
|
||||
|
||||
Assert.Equal(null, headers.HeaderContentLengthValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContentLengthValueClearedWhenHeadersCleared()
|
||||
{
|
||||
var headers = new FrameResponseHeaders();
|
||||
headers.HeaderContentLength = "42";
|
||||
var dictionary = (IDictionary<string, StringValues>)headers;
|
||||
|
||||
dictionary.Clear();
|
||||
|
||||
Assert.Equal(null, headers.HeaderContentLengthValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -12,11 +14,13 @@ namespace Microsoft.AspNetCore.Testing
|
|||
// Application errors are logged using 13 as the eventId.
|
||||
private const int ApplicationErrorEventId = 13;
|
||||
|
||||
public int TotalErrorsLogged { get; set; }
|
||||
public List<LogMessage> Messages { get; } = new List<LogMessage>();
|
||||
|
||||
public int CriticalErrorsLogged { get; set; }
|
||||
public int TotalErrorsLogged => Messages.Count(message => message.LogLevel == LogLevel.Error);
|
||||
|
||||
public int ApplicationErrorsLogged { get; set; }
|
||||
public int CriticalErrorsLogged => Messages.Count(message => message.LogLevel == LogLevel.Critical);
|
||||
|
||||
public int ApplicationErrorsLogged => Messages.Count(message => message.EventId.Id == ApplicationErrorEventId);
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
|
|
@ -34,20 +38,14 @@ namespace Microsoft.AspNetCore.Testing
|
|||
Console.WriteLine($"Log {logLevel}[{eventId}]: {formatter(state, exception)} {exception?.Message}");
|
||||
#endif
|
||||
|
||||
if (eventId.Id == ApplicationErrorEventId)
|
||||
{
|
||||
ApplicationErrorsLogged++;
|
||||
}
|
||||
Messages.Add(new LogMessage { LogLevel = logLevel, EventId = eventId, Exception = exception });
|
||||
}
|
||||
|
||||
if (logLevel == LogLevel.Error)
|
||||
{
|
||||
TotalErrorsLogged++;
|
||||
}
|
||||
|
||||
if (logLevel == LogLevel.Critical)
|
||||
{
|
||||
CriticalErrorsLogged++;
|
||||
}
|
||||
public class LogMessage
|
||||
{
|
||||
public LogLevel LogLevel { get; set; }
|
||||
public EventId EventId { get; set; }
|
||||
public Exception Exception { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,6 @@ namespace Microsoft.AspNetCore.Testing
|
|||
{
|
||||
}
|
||||
|
||||
public int HeadResponseWrites { get; set; }
|
||||
|
||||
public int HeadResponseWriteByteCount { get; set; }
|
||||
|
||||
public override void ConnectionRead(string connectionId, int count)
|
||||
{
|
||||
//_logger.LogDebug(1, @"Connection id ""{ConnectionId}"" recv {count} bytes.", connectionId, count);
|
||||
|
|
@ -31,11 +27,5 @@ namespace Microsoft.AspNetCore.Testing
|
|||
{
|
||||
//_logger.LogDebug(1, @"Connection id ""{ConnectionId}"" send finished with status {status}.", connectionId, status);
|
||||
}
|
||||
|
||||
public override void ConnectionHeadResponseBodyWrite(string connectionId, int count)
|
||||
{
|
||||
HeadResponseWrites++;
|
||||
HeadResponseWriteByteCount = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.GeneratedCode
|
|||
return values.Any() ? values.Select(formatter).Aggregate((a, b) => a + b) : "";
|
||||
}
|
||||
|
||||
static string If(bool condition, Func<string> formatter)
|
||||
{
|
||||
return condition ? formatter() : "";
|
||||
}
|
||||
|
||||
class KnownHeader
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
|
@ -228,7 +233,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
return StringValues.Empty;
|
||||
}}
|
||||
set
|
||||
{{
|
||||
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
|
||||
_contentLength = ParseContentLength(value);")}
|
||||
{header.SetBit()};
|
||||
_headers._{header.Identifier} = value; {(header.EnhancedSetter == false ? "" : $@"
|
||||
_headers._raw{header.Identifier} = null;")}
|
||||
|
|
@ -304,7 +310,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
case {byLength.Key}:
|
||||
{{{Each(byLength, header => $@"
|
||||
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase))
|
||||
{{
|
||||
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
|
||||
_contentLength = ParseContentLength(value);")}
|
||||
{header.SetBit()};
|
||||
_headers._{header.Identifier} = value;{(header.EnhancedSetter == false ? "" : $@"
|
||||
_headers._raw{header.Identifier} = null;")}
|
||||
|
|
@ -328,7 +335,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
if ({header.TestBit()})
|
||||
{{
|
||||
ThrowDuplicateKeyException();
|
||||
}}
|
||||
}}{
|
||||
If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
|
||||
_contentLength = ParseContentLength(value);")}
|
||||
{header.SetBit()};
|
||||
_headers._{header.Identifier} = value;{(header.EnhancedSetter == false ? "" : $@"
|
||||
_headers._raw{header.Identifier} = null;")}
|
||||
|
|
@ -349,7 +358,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase))
|
||||
{{
|
||||
if ({header.TestBit()})
|
||||
{{
|
||||
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
|
||||
_contentLength = null;")}
|
||||
{header.ClearBit()};
|
||||
_headers._{header.Identifier} = StringValues.Empty;{(header.EnhancedSetter == false ? "" : $@"
|
||||
_headers._raw{header.Identifier} = null;")}
|
||||
|
|
@ -369,6 +379,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
{{
|
||||
_bits = 0;
|
||||
_headers = default(HeaderReferences);
|
||||
{(loop.ClassName == "FrameResponseHeaders" ? "_contentLength = null;" : "")}
|
||||
MaybeUnknown?.Clear();
|
||||
}}
|
||||
|
||||
|
|
@ -435,7 +446,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
|
|||
_headers._{header.Identifier} = AppendValue(_headers._{header.Identifier}, value);
|
||||
}}
|
||||
else
|
||||
{{
|
||||
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
|
||||
_contentLength = ParseContentLength(value);")}
|
||||
{header.SetBit()};
|
||||
_headers._{header.Identifier} = new StringValues(value);{(header.EnhancedSetter == false ? "" : $@"
|
||||
_headers._raw{header.Identifier} = null;")}
|
||||
|
|
|
|||
Loading…
Reference in New Issue