diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index 6b601fdcc7be..426f1c7b31c7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -198,6 +198,17 @@ private async Task WriteToOutputPipe() // Now check the connection window actual = CheckConnectionWindow(actual); + // actual is negative means window size has become negative + // this can usually happen if the receiver decreases window size before receiving the previous data frame + // in this case, reset to 0 and continue, no data will be sent but will wait for window update + // RFC 9113 section 6.9.2 specifically calls out that the window size can go negative. As required, + // we continue to track the negative value but use 0 for the remainder of this write to avoid + // out-of-range errors. + if (actual < 0) + { + actual = 0; + } + // Write what we can if (actual < buffer.Length) { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index faebf3dcec64..7733bd8a591b 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -4751,6 +4751,52 @@ await ExpectAsync(Http2FrameType.DATA, Assert.True(_helloWorldBytes.AsSpan(9, 3).SequenceEqual(dataFrame3.PayloadSequence.ToArray())); } + [Fact] + public async Task WINDOW_UPDATE_Received_OnStream_Resumed_WhenInitialWindowSizeNegativeMidStream() + { + const int windowSize = 3; + _clientSettings.InitialWindowSize = windowSize; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await InitializeConnectionAsync(async context => + { + var bodyControlFeature = context.Features.Get(); + bodyControlFeature.AllowSynchronousIO = true; + await context.Response.Body.WriteAsync(new byte[windowSize - 1], 0, windowSize - 1); + await tcs.Task; + await context.Response.Body.WriteAsync(new byte[windowSize], 0, windowSize); + }); + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 32, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + + // Decrease window size after server has already sent the current window - 1 size of data + _clientSettings.InitialWindowSize = windowSize - 2; + await SendSettingsAsync(); + await ExpectAsync(Http2FrameType.DATA, + withLength: windowSize - 1, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 0); + tcs.SetResult(); + + // send window update to receive the next frame data + await SendWindowUpdateAsync(1, windowSize + 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: windowSize, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)Http2DataFrameFlags.END_STREAM, + withStreamId: 1); + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + } + [Fact] public async Task CONTINUATION_Received_Decoded() {