diff --git a/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs b/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs index 61ec44467d..6ff341bb71 100644 --- a/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs +++ b/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs @@ -14,7 +14,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { public class Http1OutputProducer : IHttpOutputProducer { - private static readonly ArraySegment _emptyData = new ArraySegment(new byte[0]); private static readonly ArraySegment _continueBytes = new ArraySegment(Encoding.ASCII.GetBytes("HTTP/1.1 100 Continue\r\n\r\n")); private static readonly byte[] _bytesHttpVersion11 = Encoding.ASCII.GetBytes("HTTP/1.1 "); private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n"); @@ -71,7 +70,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) { - return WriteAsync(_emptyData, cancellationToken); + return WriteAsync(Constants.EmptyData, cancellationToken); } public void Write(Action callback, T state) diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs index 269cc034e2..7baa89c2c7 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private readonly Http2Frame _incomingFrame = new Http2Frame(); private Http2Stream _currentHeadersStream; - private int _lastStreamId; + private int _highestOpenedStreamId; private bool _stopping; @@ -156,7 +156,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 stream.Abort(error); } - await _frameWriter.WriteGoAwayAsync(_lastStreamId, errorCode); + await _frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, errorCode); } finally { @@ -247,16 +247,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR); } - if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream) && !stream.HasReceivedEndStream) + ThrowIfIncomingFrameSentToIdleStream(); + + if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream) && !stream.EndStreamReceived) { return stream.OnDataAsync(_incomingFrame.DataPayload, endStream: (_incomingFrame.DataFlags & Http2DataFrameFlags.END_STREAM) == Http2DataFrameFlags.END_STREAM); } - return _frameWriter.WriteRstStreamAsync(_incomingFrame.StreamId, Http2ErrorCode.STREAM_CLOSED); + // http://httpwg.org/specs/rfc7540.html#rfc.section.5.1 + // + // ...an endpoint that receives any frames after receiving a frame with the + // END_STREAM flag set MUST treat that as a connection error (Section 5.4.1) + // of type STREAM_CLOSED, unless the frame is permitted as described below. + // + // (The allowed frame types for this situation are WINDOW_UPDATE, RST_STREAM and PRIORITY) + // + // If we couldn't find the stream, it was either alive previously but closed with + // END_STREAM or RST_STREAM, or it was implicitly closed when the client opened + // a new stream with a higher ID. Per the spec, we should send RST_STREAM if + // the stream was closed with RST_STREAM or implicitly, but the spec also says + // in http://httpwg.org/specs/rfc7540.html#rfc.section.5.4.1 that + // + // An endpoint can end a connection at any time. In particular, an endpoint MAY + // choose to treat a stream error as a connection error. + // + // We choose to do that here so we don't have to keep state to track implicitly closed + // streams vs. streams closed with END_STREAM or RST_STREAM. + throw new Http2ConnectionErrorException(Http2ErrorCode.STREAM_CLOSED); } - private Task ProcessHeadersFrameAsync(IHttpApplication application) + private async Task ProcessHeadersFrameAsync(IHttpApplication application) { if (_currentHeadersStream != null) { @@ -278,33 +299,66 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR); } - _currentHeadersStream = new Http2Stream(application, new Http2StreamContext + if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream)) { - ConnectionId = ConnectionId, - StreamId = _incomingFrame.StreamId, - ServiceContext = _context.ServiceContext, - ConnectionFeatures = _context.ConnectionFeatures, - PipeFactory = _context.PipeFactory, - LocalEndPoint = _context.LocalEndPoint, - RemoteEndPoint = _context.RemoteEndPoint, - StreamLifetimeHandler = this, - FrameWriter = _frameWriter - }); - _currentHeadersStream.ExpectData = (_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == 0; - _currentHeadersStream.Reset(); - - _streams[_incomingFrame.StreamId] = _currentHeadersStream; - - _hpackDecoder.Decode(_incomingFrame.HeadersPayload, _currentHeadersStream.RequestHeaders); - - if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS) - { - _lastStreamId = _incomingFrame.StreamId; - _ = _currentHeadersStream.ProcessRequestsAsync(); - _currentHeadersStream = null; + // http://httpwg.org/specs/rfc7540.html#rfc.section.5.1 + // + // ...an endpoint that receives any frames after receiving a frame with the + // END_STREAM flag set MUST treat that as a connection error (Section 5.4.1) + // of type STREAM_CLOSED, unless the frame is permitted as described below. + // + // (The allowed frame types for this situation are WINDOW_UPDATE, RST_STREAM and PRIORITY) + if (stream.EndStreamReceived) + { + throw new Http2ConnectionErrorException(Http2ErrorCode.STREAM_CLOSED); + } + // TODO: trailers } + else if (_incomingFrame.StreamId <= _highestOpenedStreamId) + { + // http://httpwg.org/specs/rfc7540.html#rfc.section.5.1.1 + // + // The first use of a new stream identifier implicitly closes all streams in the "idle" + // state that might have been initiated by that peer with a lower-valued stream identifier. + // + // If we couldn't find the stream, it was previously closed (either implicitly or with + // END_STREAM or RST_STREAM). + throw new Http2ConnectionErrorException(Http2ErrorCode.STREAM_CLOSED); + } + else + { + // Start a new stream + _currentHeadersStream = new Http2Stream(application, new Http2StreamContext + { + ConnectionId = ConnectionId, + StreamId = _incomingFrame.StreamId, + ServiceContext = _context.ServiceContext, + ConnectionFeatures = _context.ConnectionFeatures, + PipeFactory = _context.PipeFactory, + LocalEndPoint = _context.LocalEndPoint, + RemoteEndPoint = _context.RemoteEndPoint, + StreamLifetimeHandler = this, + FrameWriter = _frameWriter + }); - return Task.CompletedTask; + if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM) + { + await _currentHeadersStream.OnDataAsync(Constants.EmptyData, endStream: true); + } + + _currentHeadersStream.Reset(); + + _streams[_incomingFrame.StreamId] = _currentHeadersStream; + + _hpackDecoder.Decode(_incomingFrame.HeadersPayload, _currentHeadersStream.RequestHeaders); + + if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS) + { + _highestOpenedStreamId = _incomingFrame.StreamId; + _ = _currentHeadersStream.ProcessRequestsAsync(); + _currentHeadersStream = null; + } + } } private Task ProcessPriorityFrameAsync() @@ -349,14 +403,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(Http2ErrorCode.FRAME_SIZE_ERROR); } + ThrowIfIncomingFrameSentToIdleStream(); + if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream)) { stream.Abort(error: null); } - else if (_incomingFrame.StreamId > _lastStreamId) - { - throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR); - } return Task.CompletedTask; } @@ -449,16 +501,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(Http2ErrorCode.FRAME_SIZE_ERROR); } - if (_incomingFrame.StreamId == 0) + ThrowIfIncomingFrameSentToIdleStream(); + + if (_incomingFrame.WindowUpdateSizeIncrement == 0) { - if (_incomingFrame.WindowUpdateSizeIncrement == 0) + if (_incomingFrame.StreamId == 0) { throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR); } - } - else - { - if (_incomingFrame.WindowUpdateSizeIncrement == 0) + else { return _frameWriter.WriteRstStreamAsync(_incomingFrame.StreamId, Http2ErrorCode.PROTOCOL_ERROR); } @@ -478,7 +529,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 if ((_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS) { - _lastStreamId = _currentHeadersStream.StreamId; + _highestOpenedStreamId = _currentHeadersStream.StreamId; _ = _currentHeadersStream.ProcessRequestsAsync(); _currentHeadersStream = null; } @@ -496,6 +547,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return Task.CompletedTask; } + private void ThrowIfIncomingFrameSentToIdleStream() + { + // http://httpwg.org/specs/rfc7540.html#rfc.section.5.1 + // 5.1. Stream states + // ... + // idle: + // ... + // Receiving any frame other than HEADERS or PRIORITY on a stream in this state MUST be + // treated as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. + // + // If the stream ID in the incoming frame is higher than the highest opened stream ID so + // far, then the incoming frame's target stream is in the idle state, which is the implicit + // initial state for all streams. + if (_incomingFrame.StreamId > _highestOpenedStreamId) + { + throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR); + } + } + void IHttp2StreamLifetimeHandler.OnStreamCompleted(int streamId) { _streams.TryRemove(streamId, out _); diff --git a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs index a8c85e7b68..fb2468d0c4 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs @@ -7,16 +7,13 @@ using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; -using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { public class Http2FrameWriter : IHttp2FrameWriter { - private static readonly ArraySegment _emptyData = new ArraySegment(new byte[0]); - private readonly Http2Frame _outgoingFrame = new Http2Frame(); private readonly object _writeLock = new object(); private readonly HPackEncoder _hpackEncoder = new HPackEncoder(); @@ -48,7 +45,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public Task FlushAsync(CancellationToken cancellationToken) { - return WriteAsync(_emptyData); + return WriteAsync(Constants.EmptyData); } public Task Write100ContinueAsync(int streamId) diff --git a/src/Kestrel.Core/Internal/Http2/Http2MessageBody.cs b/src/Kestrel.Core/Internal/Http2/Http2MessageBody.cs index 26013948dc..71a308a8f7 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2MessageBody.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2MessageBody.cs @@ -29,9 +29,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 HttpRequestHeaders headers, Http2Stream context) { - if (!context.ExpectData) + if (context.EndStreamReceived) { - return MessageBody.ZeroContentLengthClose; + return ZeroContentLengthClose; } return new ForHttp2(context); diff --git a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs index 1b7d43b284..93f549b5ce 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs @@ -6,13 +6,12 @@ using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { public class Http2OutputProducer : IHttpOutputProducer { - private static readonly ArraySegment _emptyData = new ArraySegment(new byte[0]); - private readonly int _streamId; private readonly IHttp2FrameWriter _frameWriter; @@ -47,7 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public Task WriteStreamSuffixAsync(CancellationToken cancellationToken) { - return _frameWriter.WriteDataAsync(_streamId, _emptyData, endStream: true, cancellationToken: cancellationToken); + return _frameWriter.WriteDataAsync(_streamId, Constants.EmptyData, endStream: true, cancellationToken: cancellationToken); } public void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders) diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs index 71457fae4c..b8f9db04ee 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs @@ -24,12 +24,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public int StreamId => _context.StreamId; - public bool HasReceivedEndStream { get; private set; } + public bool EndStreamReceived { get; private set; } protected IHttp2StreamLifetimeHandler StreamLifetimeHandler => _context.StreamLifetimeHandler; - public bool ExpectData { get; set; } - public override bool IsUpgradableRequest => false; protected override void OnReset() @@ -91,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 if (endStream) { - HasReceivedEndStream = true; + EndStreamReceived = true; RequestBodyPipe.Writer.Complete(); } } diff --git a/src/Kestrel.Core/Internal/Infrastructure/Constants.cs b/src/Kestrel.Core/Internal/Infrastructure/Constants.cs index 7f93242028..6db5d384ee 100644 --- a/src/Kestrel.Core/Internal/Infrastructure/Constants.cs +++ b/src/Kestrel.Core/Internal/Infrastructure/Constants.cs @@ -32,5 +32,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure public const string ServerName = "Kestrel"; public static readonly TimeSpan RequestBodyDrainTimeout = TimeSpan.FromSeconds(5); + + public static readonly ArraySegment EmptyData = new ArraySegment(new byte[0]); } } diff --git a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs index 0de04f62df..873233df34 100644 --- a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs +++ b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs @@ -467,33 +467,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } [Fact] - public async Task DATA_Received_StreamIdle_StreamError() + public async Task DATA_Received_StreamIdle_ConnectionError() { await InitializeConnectionAsync(_noopApplication); await SendDataAsync(1, _helloWorldBytes, endStream: false); - await WaitForStreamErrorAsync(expectedStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonRstStreamFrames: false); - - await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false); } [Fact] - public async Task DATA_Received_StreamHalfClosedRemote_StreamError() + public async Task DATA_Received_StreamHalfClosedRemote_ConnectionError() { - await InitializeConnectionAsync(_echoWaitForAbortApplication); + // Use _waitForAbortApplication so we know the stream will still be active when we send the illegal DATA frame + await InitializeConnectionAsync(_waitForAbortApplication); - await StartStreamAsync(1, _postRequestHeaders, endStream: false); - await SendDataAsync(1, _helloBytes, endStream: true); - await SendDataAsync(1, _worldBytes, endStream: true); + await StartStreamAsync(1, _postRequestHeaders, endStream: true); - await WaitForStreamErrorAsync(expectedStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonRstStreamFrames: true); + await SendDataAsync(1, _helloWorldBytes, endStream: false); - await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: true); + await WaitForConnectionErrorAsync(expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false); } [Fact] - public async Task DATA_Received_StreamClosed_StreamError() + public async Task DATA_Received_StreamClosed_ConnectionError() { await InitializeConnectionAsync(_noopApplication); @@ -510,13 +507,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloWorldBytes, endStream: false); - await WaitForStreamErrorAsync(expectedStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonRstStreamFrames: false); - - await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + await WaitForConnectionErrorAsync(expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false); } [Fact] - public async Task DATA_Received_StreamClosedImplicitly_StreamError() + public async Task DATA_Received_StreamClosedImplicitly_ConnectionError() { // http://httpwg.org/specs/rfc7540.html#rfc.section.5.1.1 // @@ -540,9 +535,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await SendDataAsync(1, _helloWorldBytes, endStream: true); - await WaitForStreamErrorAsync(expectedStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonRstStreamFrames: false); - - await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + await WaitForConnectionErrorAsync(expectedLastStreamId: 3, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false); } [Fact] @@ -655,6 +648,63 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false); } + [Fact] + public async Task HEADERS_Received_StreamClosed_ConnectionError() + { + await InitializeConnectionAsync(_noopApplication); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + 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); + + // Try to re-use the stream ID (http://httpwg.org/specs/rfc7540.html#rfc.section.5.1.1) + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + await WaitForConnectionErrorAsync(expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task HEADERS_Received_StreamHalfClosedRemote_ConnectionError() + { + // Use _waitForAbortApplication so we know the stream will still be active when we send the illegal DATA frame + await InitializeConnectionAsync(_waitForAbortApplication); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + await SendHeadersAsync(1, Http2HeadersFrameFlags.NONE, _browserRequestHeaders); + + await WaitForConnectionErrorAsync(expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task HEADERS_Received_StreamClosedImplicitly_ConnectionError() + { + await InitializeConnectionAsync(_noopApplication); + + await StartStreamAsync(3, _browserRequestHeaders, endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 3); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 3); + + // Stream 1 was implicitly closed by opening stream 3 before (http://httpwg.org/specs/rfc7540.html#rfc.section.5.1.1) + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + await WaitForConnectionErrorAsync(expectedLastStreamId: 3, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false); + } + [Theory] [InlineData(0)] [InlineData(1)] @@ -811,6 +861,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false); } + [Fact] + public async Task RST_STREAM_Received_StreamIdle_ConnectionError() + { + await InitializeConnectionAsync(_noopApplication); + + await SendRstStreamAsync(1); + + await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false); + } + [Theory] [InlineData(3)] [InlineData(5)] @@ -1077,6 +1137,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false); } + [Fact] + public async Task WINDOW_UPDATE_Received_StreamIdle_ConnectionError() + { + await InitializeConnectionAsync(_waitForAbortApplication); + + await SendWindowUpdateAsync(1, sizeIncrement: 1); + + await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false); + } + [Fact] public async Task WINDOW_UPDATE_Received_OnStream_SizeIncrementZero_StreamError() {