Send RST for canceled HTTP/2 writes #3007
This commit is contained in:
parent
d3f2ca9c9a
commit
23a4e11261
|
|
@ -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.
|
// This is called when a CancellationToken fires mid-write. In HTTP/1.x, this aborts the entire connection.
|
||||||
// Should we do that here?
|
// For HTTP/2 we abort the stream.
|
||||||
void IHttpOutputAborter.Abort(ConnectionAbortedException abortReason)
|
void IHttpOutputAborter.Abort(ConnectionAbortedException abortReason)
|
||||||
{
|
{
|
||||||
|
_stream.ResetAndAbort(abortReason, Http2ErrorCode.INTERNAL_ERROR);
|
||||||
Dispose();
|
Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -427,7 +427,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
ResetAndAbort(abortReason, Http2ErrorCode.INTERNAL_ERROR);
|
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.
|
// Future incoming frames will drain for a default grace period to avoid destabilizing the connection.
|
||||||
var states = ApplyCompletionFlag(StreamCompletionFlags.Aborted);
|
var states = ApplyCompletionFlag(StreamCompletionFlags.Aborted);
|
||||||
|
|
|
||||||
|
|
@ -1998,5 +1998,90 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
await WaitForConnectionErrorAsync<HPackEncodingException>(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR,
|
await WaitForConnectionErrorAsync<HPackEncodingException>(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR,
|
||||||
CoreStrings.HPackErrorNotEnoughBuffer);
|
CoreStrings.HPackErrorNotEnoughBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WriteAsync_PreCancelledCancellationToken_DoesNotAbort()
|
||||||
|
{
|
||||||
|
var headers = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||||
|
new KeyValuePair<string, string>(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<OperationCanceledException>(() => 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<int>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var headers = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||||
|
new KeyValuePair<string, string>(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<OperationCanceledException>(() => 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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue