diff --git a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs index 0bdf5186a8..fa31210932 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs @@ -74,10 +74,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - // Review: This is called when a CancellationToken fires mid-write. In HTTP/1.x, this aborts the entire connection. - // Should we do that here? + // This is called when a CancellationToken fires mid-write. In HTTP/1.x, this aborts the entire connection. + // For HTTP/2 we abort the stream. void IHttpOutputAborter.Abort(ConnectionAbortedException abortReason) { + _stream.ResetAndAbort(abortReason, Http2ErrorCode.INTERNAL_ERROR); Dispose(); } diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs index 25623576ed..06f51fd096 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs @@ -427,7 +427,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 ResetAndAbort(abortReason, Http2ErrorCode.INTERNAL_ERROR); } - private void ResetAndAbort(ConnectionAbortedException abortReason, Http2ErrorCode error) + internal void ResetAndAbort(ConnectionAbortedException abortReason, Http2ErrorCode error) { // Future incoming frames will drain for a default grace period to avoid destabilizing the connection. var states = ApplyCompletionFlag(StreamCompletionFlags.Aborted); diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs index 1b94337efc..8375561f9b 100644 --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -1998,5 +1998,90 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR, CoreStrings.HPackErrorNotEnoughBuffer); } + + [Fact] + public async Task WriteAsync_PreCancelledCancellationToken_DoesNotAbort() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(async context => + { + // The cancellation is checked at the start of WriteAsync and no application state is changed. + await Assert.ThrowsAsync(() => context.Response.WriteAsync("hello,", new CancellationToken(true))); + Assert.False(context.Response.HasStarted); + }); + + await StartStreamAsync(1, headers, 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.PayloadSequence, 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 WriteAsync_CancellationTokenTriggeredDueToFlowControl_SendRST() + { + var cts = new CancellationTokenSource(); + var writeStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(async context => + { + await context.Response.Body.FlushAsync(); // https://github.com/aspnet/KestrelHttpServer/issues/3031 + var writeTask = context.Response.WriteAsync("hello,", cts.Token); + writeStarted.SetResult(0); + await Assert.ThrowsAsync(() => writeTask); + }); + + _clientSettings.InitialWindowSize = 0; + await SendSettingsAsync(); + await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 0, + withFlags: (byte)Http2SettingsFrameFlags.ACK, + withStreamId: 0); + + await StartStreamAsync(1, headers, endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + + await writeStarted.Task; + + cts.Cancel(); + + await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, null); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); + + Assert.Equal(2, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + } } } \ No newline at end of file