diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx
index 7bbbdfff3a..66048b1b15 100644
--- a/src/Kestrel.Core/CoreStrings.resx
+++ b/src/Kestrel.Core/CoreStrings.resx
@@ -557,4 +557,10 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
The request :scheme header '{requestScheme}' does not match the transport scheme '{transportScheme}'.
+
+ Less data received than specified in the Content-Length header.
+
+
+ More data received than specified in the Content-Length header.
+
\ No newline at end of file
diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
index ae457545a1..487b8fae27 100644
--- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
+++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
@@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private Http2Stream _currentHeadersStream;
private RequestHeaderParsingState _requestHeaderParsingState;
private PseudoHeaderFields _parsedPseudoHeaderFields;
+ private Http2HeadersFrameFlags _headerFlags;
private bool _isMethodConnect;
private int _highestOpenedStreamId;
@@ -469,12 +470,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
TimeoutControl = this,
});
- if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM)
- {
- _currentHeadersStream.OnEndStreamReceived();
- }
-
_currentHeadersStream.Reset();
+ _headerFlags = _incomingFrame.HeadersFlags;
var endHeaders = (_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS;
await DecodeHeadersAsync(application, endHeaders, _incomingFrame.HeadersPayload);
@@ -768,6 +765,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorMissingMandatoryPseudoHeaderFields, Http2ErrorCode.PROTOCOL_ERROR);
}
+ // This must be initialized before we offload the request or else we may start processing request body frames without it.
+ _currentHeadersStream.InputRemaining = _currentHeadersStream.RequestHeaders.ContentLength;
+
+ // This must wait until we've received all of the headers so we can verify the content-length.
+ if ((_headerFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM)
+ {
+ _currentHeadersStream.OnEndStreamReceived();
+ }
+
_streams[_incomingFrame.StreamId] = _currentHeadersStream;
// Must not allow app code to block the connection handling loop.
ThreadPool.UnsafeQueueUserWorkItem(state =>
@@ -788,6 +794,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_currentHeadersStream = null;
_requestHeaderParsingState = RequestHeaderParsingState.Ready;
_parsedPseudoHeaderFields = PseudoHeaderFields.None;
+ _headerFlags = Http2HeadersFrameFlags.NONE;
_isMethodConnect = false;
}
diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
index a0828e7ed2..48699e84ae 100644
--- a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
+++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
@@ -48,8 +48,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public int StreamId => _context.StreamId;
+ public long? InputRemaining { get; internal set; }
+
public bool RequestBodyStarted { get; private set; }
public bool EndStreamReceived => (_completionState & StreamCompletionFlags.EndStreamReceived) == StreamCompletionFlags.EndStreamReceived;
+ private bool IsAborted => (_completionState & StreamCompletionFlags.Aborted) == StreamCompletionFlags.Aborted;
public override bool IsUpgradableRequest => false;
@@ -263,8 +266,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public Task OnDataAsync(Http2Frame dataFrame)
{
- // TODO: content-length accounting
-
// Since padding isn't buffered, immediately count padding bytes as read for flow control purposes.
if (dataFrame.DataHasPadding)
{
@@ -288,6 +289,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_inputFlowControl.Advance(payload.Count);
+ if (IsAborted)
+ {
+ // Ignore data frames for aborted streams, but only after counting them for purposes of connection level flow control.
+ return Task.CompletedTask;
+ }
+
+ // This check happens after flow control so that when we throw and abort, the byte count is returned to the connection
+ // level accounting.
+ if (InputRemaining.HasValue)
+ {
+ // https://tools.ietf.org/html/rfc7540#section-8.1.2.6
+ if (payload.Count > InputRemaining.Value)
+ {
+ throw new Http2StreamErrorException(StreamId, CoreStrings.Http2StreamErrorMoreDataThanLength, Http2ErrorCode.PROTOCOL_ERROR);
+ }
+
+ InputRemaining -= payload.Count;
+ }
+
RequestBodyPipe.Writer.Write(payload);
var flushTask = RequestBodyPipe.Writer.FlushAsync();
@@ -306,6 +326,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public void OnEndStreamReceived()
{
+ if (InputRemaining.HasValue)
+ {
+ // https://tools.ietf.org/html/rfc7540#section-8.1.2.6
+ if (InputRemaining.Value != 0)
+ {
+ throw new Http2StreamErrorException(StreamId, CoreStrings.Http2StreamErrorLessDataThanLength, Http2ErrorCode.PROTOCOL_ERROR);
+ }
+ }
+
TryApplyCompletionFlag(StreamCompletionFlags.EndStreamReceived);
RequestBodyPipe.Writer.Complete();
diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs
index 09e593c57b..8b977316ec 100644
--- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs
+++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs
@@ -2058,6 +2058,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHttp2StreamErrorSchemeMismatch(object requestScheme, object transportScheme)
=> string.Format(CultureInfo.CurrentCulture, GetString("Http2StreamErrorSchemeMismatch", "requestScheme", "transportScheme"), requestScheme, transportScheme);
+ ///
+ /// Less data received than specified in the Content-Length header.
+ ///
+ internal static string Http2StreamErrorLessDataThanLength
+ {
+ get => GetString("Http2StreamErrorLessDataThanLength");
+ }
+
+ ///
+ /// Less data received than specified in the Content-Length header.
+ ///
+ internal static string FormatHttp2StreamErrorLessDataThanLength()
+ => GetString("Http2StreamErrorLessDataThanLength");
+
+ ///
+ /// More data received than specified in the Content-Length header.
+ ///
+ internal static string Http2StreamErrorMoreDataThanLength
+ {
+ get => GetString("Http2StreamErrorMoreDataThanLength");
+ }
+
+ ///
+ /// More data received than specified in the Content-Length header.
+ ///
+ internal static string FormatHttp2StreamErrorMoreDataThanLength()
+ => GetString("Http2StreamErrorMoreDataThanLength");
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/test/Kestrel.Core.Tests/Http2StreamTests.cs b/test/Kestrel.Core.Tests/Http2StreamTests.cs
index da8e98b987..c908ee8031 100644
--- a/test/Kestrel.Core.Tests/Http2StreamTests.cs
+++ b/test/Kestrel.Core.Tests/Http2StreamTests.cs
@@ -42,11 +42,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
new KeyValuePair("upgrade-insecure-requests", "1"),
};
- private readonly MemoryPool _memoryPool = KestrelMemoryPool.Create();
- private readonly DuplexPipe.DuplexPipePair _pair;
+ private MemoryPool _memoryPool = KestrelMemoryPool.Create();
+ private DuplexPipe.DuplexPipePair _pair;
private readonly TestApplicationErrorLogger _logger;
- private readonly Http2ConnectionContext _connectionContext;
- private readonly Http2Connection _connection;
+ private Http2ConnectionContext _connectionContext;
+ private Http2Connection _connection;
private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();
private readonly HPackEncoder _hpackEncoder = new HPackEncoder();
private readonly HPackDecoder _hpackDecoder;
@@ -68,24 +68,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public Http2StreamTests()
{
- // Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing
- // Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make
- // it easier to verify request frames are processed correctly immediately after sending the them.
- var inputPipeOptions = new PipeOptions(
- pool: _memoryPool,
- readerScheduler: PipeScheduler.Inline,
- writerScheduler: PipeScheduler.ThreadPool,
- useSynchronizationContext: false
- );
- var outputPipeOptions = new PipeOptions(
- pool: _memoryPool,
- readerScheduler: PipeScheduler.ThreadPool,
- writerScheduler: PipeScheduler.Inline,
- useSynchronizationContext: false
- );
-
- _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
-
_noopApplication = context => Task.CompletedTask;
_echoMethod = context =>
@@ -178,6 +160,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_logger = new TestApplicationErrorLogger();
+ InitializeConnectionFields(KestrelMemoryPool.Create());
+ }
+
+ private void InitializeConnectionFields(MemoryPool memoryPool)
+ {
+ _memoryPool = memoryPool;
+
+ // Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing
+ // Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make
+ // it easier to verify request frames are processed correctly immediately after sending the them.
+ var inputPipeOptions = new PipeOptions(
+ pool: _memoryPool,
+ readerScheduler: PipeScheduler.Inline,
+ writerScheduler: PipeScheduler.ThreadPool,
+ useSynchronizationContext: false
+ );
+ var outputPipeOptions = new PipeOptions(
+ pool: _memoryPool,
+ readerScheduler: PipeScheduler.ThreadPool,
+ writerScheduler: PipeScheduler.Inline,
+ useSynchronizationContext: false
+ );
+
+ _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
+
_connectionContext = new Http2ConnectionContext
{
ConnectionFeatures = new FeatureCollection(),
@@ -754,6 +761,308 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
}
+ [Fact]
+ public async Task ContentLength_Received_SingleDataFrame_Verified()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "POST"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.ContentLength, "12"),
+ };
+ await InitializeConnectionAsync(async context =>
+ {
+ var buffer = new byte[100];
+ var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length);
+ Assert.Equal(12, read);
+ });
+
+ await StartStreamAsync(1, headers, endStream: false);
+ await SendDataAsync(1, new byte[12].AsSpan(), endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 55,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(3, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
+ }
+
+ [Fact]
+ public async Task ContentLength_ReceivedInContinuation_SingleDataFrame_Verified()
+ {
+ await InitializeConnectionAsync(async context =>
+ {
+ var buffer = new byte[100];
+ var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length);
+ Assert.Equal(12, read);
+ });
+
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "POST"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair("a", _largeHeaderValue),
+ new KeyValuePair("b", _largeHeaderValue),
+ new KeyValuePair("c", _largeHeaderValue),
+ new KeyValuePair("d", _largeHeaderValue),
+ new KeyValuePair(HeaderNames.ContentLength, "12"),
+ };
+ await StartStreamAsync(1, headers, endStream: false);
+ await SendDataAsync(1, new byte[12].AsSpan(), endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 55,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(3, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
+ }
+
+ [Fact]
+ public async Task ContentLength_Received_MultipleDataFrame_Verified()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "POST"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.ContentLength, "12"),
+ };
+ await InitializeConnectionAsync(async context =>
+ {
+ var buffer = new byte[100];
+ var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length);
+ var total = read;
+ while (read > 0)
+ {
+ read = await context.Request.Body.ReadAsync(buffer, total, buffer.Length - total);
+ total += read;
+ }
+ Assert.Equal(12, total);
+ });
+
+
+ await StartStreamAsync(1, headers, endStream: false);
+ await SendDataAsync(1, new byte[1].AsSpan(), endStream: false);
+ await SendDataAsync(1, new byte[3].AsSpan(), endStream: false);
+ await SendDataAsync(1, new byte[8].AsSpan(), endStream: true);
+
+ var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 55,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 1);
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
+
+ Assert.Equal(3, _decodedHeaders.Count);
+ Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+ Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+ Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
+ }
+
+ [Fact]
+ public async Task ContentLength_Received_NoDataFrames_Reset()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "POST"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.ContentLength, "12"),
+ };
+ await InitializeConnectionAsync(_noopApplication);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorLessDataThanLength);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task ContentLength_ReceivedInContinuation_NoDataFrames_Reset()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "POST"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair("a", _largeHeaderValue),
+ new KeyValuePair("b", _largeHeaderValue),
+ new KeyValuePair("c", _largeHeaderValue),
+ new KeyValuePair("d", _largeHeaderValue),
+ new KeyValuePair(HeaderNames.ContentLength, "12"),
+ };
+ await InitializeConnectionAsync(_noopApplication);
+
+ await StartStreamAsync(1, headers, endStream: true);
+
+ await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorLessDataThanLength);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task ContentLength_Received_SingleDataFrameOverSize_Reset()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "POST"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.ContentLength, "12"),
+ };
+ await InitializeConnectionAsync(async context =>
+ {
+ await Assert.ThrowsAsync(async () =>
+ {
+ var buffer = new byte[100];
+ while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { }
+ });
+ });
+
+ await StartStreamAsync(1, headers, endStream: false);
+ await SendDataAsync(1, new byte[13].AsSpan(), endStream: true);
+
+ await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorMoreDataThanLength);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task ContentLength_Received_SingleDataFrameUnderSize_Reset()
+ {
+ // I hate doing this, but it avoids exceptions from MemoryPool.Dipose() in debug mode. The problem is since
+ // the stream's ProcessRequestsAsync loop is never awaited by the connection, it's not really possible to
+ // observe when all the blocks are returned. This can be removed after we implement graceful shutdown.
+ Dispose();
+ InitializeConnectionFields(new DiagnosticMemoryPool(KestrelMemoryPool.CreateSlabMemoryPool(), allowLateReturn: true));
+
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "POST"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.ContentLength, "12"),
+ };
+ await InitializeConnectionAsync(async context =>
+ {
+ await Assert.ThrowsAsync(async () =>
+ {
+ var buffer = new byte[100];
+ while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { }
+ });
+ });
+
+ await StartStreamAsync(1, headers, endStream: false);
+ await SendDataAsync(1, new byte[11].AsSpan(), endStream: true);
+
+ await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorLessDataThanLength);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task ContentLength_Received_MultipleDataFramesOverSize_Reset()
+ {
+ // I hate doing this, but it avoids exceptions from MemoryPool.Dipose() in debug mode. The problem is since
+ // the stream's ProcessRequestsAsync loop is never awaited by the connection, it's not really possible to
+ // observe when all the blocks are returned. This can be removed after we implement graceful shutdown.
+ Dispose();
+ InitializeConnectionFields(new DiagnosticMemoryPool(KestrelMemoryPool.CreateSlabMemoryPool(), allowLateReturn: true));
+
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "POST"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.ContentLength, "12"),
+ };
+ await InitializeConnectionAsync(async context =>
+ {
+ await Assert.ThrowsAsync(async () =>
+ {
+ var buffer = new byte[100];
+ while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { }
+ });
+ });
+
+ await StartStreamAsync(1, headers, endStream: false);
+ await SendDataAsync(1, new byte[1].AsSpan(), endStream: false);
+ await SendDataAsync(1, new byte[2].AsSpan(), endStream: false);
+ await SendDataAsync(1, new byte[10].AsSpan(), endStream: false);
+ await SendDataAsync(1, new byte[2].AsSpan(), endStream: true);
+
+ await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorMoreDataThanLength);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task ContentLength_Received_MultipleDataFramesUnderSize_Reset()
+ {
+ // I hate doing this, but it avoids exceptions from MemoryPool.Dipose() in debug mode. The problem is since
+ // the stream's ProcessRequestsAsync loop is never awaited by the connection, it's not really possible to
+ // observe when all the blocks are returned. This can be removed after we implement graceful shutdown.
+ Dispose();
+ InitializeConnectionFields(new DiagnosticMemoryPool(KestrelMemoryPool.CreateSlabMemoryPool(), allowLateReturn: true));
+
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "POST"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.ContentLength, "12"),
+ };
+ await InitializeConnectionAsync(async context =>
+ {
+ await Assert.ThrowsAsync(async () =>
+ {
+ var buffer = new byte[100];
+ while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) { }
+ });
+ });
+
+ await StartStreamAsync(1, headers, endStream: false);
+ await SendDataAsync(1, new byte[1].AsSpan(), endStream: false);
+ await SendDataAsync(1, new byte[2].AsSpan(), endStream: true);
+
+ await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.Http2StreamErrorLessDataThanLength);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
[Fact]
public async Task RST_STREAM_Received_AbortsStream()
{
@@ -1182,8 +1491,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
if (expectedErrorMessage != null)
{
- var message = Assert.Single(_logger.Messages, m => m.Exception is ConnectionAbortedException);
- Assert.Contains(expectedErrorMessage, message.Exception.Message);
+ Assert.Contains(_logger.Messages, m => m.Exception?.Message.Contains(expectedErrorMessage) ?? false);
}
}
}
diff --git a/test/Kestrel.FunctionalTests/Http2/H2SpecTests.cs b/test/Kestrel.FunctionalTests/Http2/H2SpecTests.cs
index 239d051200..b5a196c5f2 100644
--- a/test/Kestrel.FunctionalTests/Http2/H2SpecTests.cs
+++ b/test/Kestrel.FunctionalTests/Http2/H2SpecTests.cs
@@ -3,6 +3,7 @@
#if NETCOREAPP2_2
+using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
@@ -56,7 +57,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
get
{
var dataset = new TheoryData();
- var toSkip = new[] { "hpack/4.2/1", "http2/5.1/8", "http2/8.1.2.6/1", "http2/8.1.2.6/2" };
+ var toSkip = new[] { "hpack/4.2/1", "http2/5.1/8" };
foreach (var testcase in H2SpecCommands.EnumerateTestCases())
{
@@ -123,9 +124,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
private void ConfigureHelloWorld(IApplicationBuilder app)
{
- app.Run(context =>
+ app.Run(async context =>
{
- return context.Request.Body.CopyToAsync(context.Response.Body);
+ // Read the whole request body to check for errors.
+ await context.Request.Body.CopyToAsync(Stream.Null);
+ await context.Response.WriteAsync("Hello World");
});
}
}
diff --git a/test/shared/KestrelTestLoggerProvider.cs b/test/shared/KestrelTestLoggerProvider.cs
index 48455557fb..69984f2770 100644
--- a/test/shared/KestrelTestLoggerProvider.cs
+++ b/test/shared/KestrelTestLoggerProvider.cs
@@ -27,7 +27,6 @@ namespace Microsoft.AspNetCore.Testing
public void Dispose()
{
- throw new NotImplementedException();
}
}
}