Skip to content

Commit 7a34dd8

Browse files
committed
Implement IHttpResetFeature in Kestrel #10886
1 parent 87ea03d commit 7a34dd8

File tree

9 files changed

+281
-1
lines changed

9 files changed

+281
-1
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Http.Features
5+
{
6+
/// <summary>
7+
/// Used to send reset messages for protocols that support them such as HTTP/2 or HTTP/3.
8+
/// </summary>
9+
public interface IHttpResetFeature
10+
{
11+
12+
/// <summary>
13+
/// Send a reset message with the given error code. The request will be considered aborted.
14+
/// </summary>
15+
/// <param name="errorCode"></param>
16+
void Reset(int errorCode);
17+
}
18+
}

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,4 +608,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
608608
<data name="HTTP2NoTlsOsx" xml:space="preserve">
609609
<value>HTTP/2 over TLS is not supported on OSX due to missing ALPN support.</value>
610610
</data>
611+
<data name="Http2StreamResetByApplication" xml:space="preserve">
612+
<value>The HTTP/2 stream was reset by the application with error code {errorCode}.</value>
613+
</data>
611614
</root>

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ protected void ResetHttp2Features()
279279
_currentIHttp2StreamIdFeature = this;
280280
_currentIHttpResponseCompletionFeature = this;
281281
_currentIHttpResponseTrailersFeature = this;
282+
_currentIHttpResetFeature = this;
282283
}
283284

284285
void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state)

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ internal partial class HttpProtocol : IFeatureCollection
4141
private static readonly Type IHttpMinResponseDataRateFeatureType = typeof(IHttpMinResponseDataRateFeature);
4242
private static readonly Type IHttpBodyControlFeatureType = typeof(IHttpBodyControlFeature);
4343
private static readonly Type IHttpResponseStartFeatureType = typeof(IHttpResponseStartFeature);
44+
private static readonly Type IHttpResetFeatureType = typeof(IHttpResetFeature);
4445
private static readonly Type IHttpSendFileFeatureType = typeof(IHttpSendFileFeature);
4546

4647
private object _currentIHttpRequestFeature;
@@ -71,6 +72,7 @@ internal partial class HttpProtocol : IFeatureCollection
7172
private object _currentIHttpMinResponseDataRateFeature;
7273
private object _currentIHttpBodyControlFeature;
7374
private object _currentIHttpResponseStartFeature;
75+
private object _currentIHttpResetFeature;
7476
private object _currentIHttpSendFileFeature;
7577

7678
private int _featureRevision;
@@ -108,6 +110,7 @@ private void FastReset()
108110
_currentIHttpWebSocketFeature = null;
109111
_currentISessionFeature = null;
110112
_currentIHttpMinResponseDataRateFeature = null;
113+
_currentIHttpResetFeature = null;
111114
_currentIHttpSendFileFeature = null;
112115
}
113116

@@ -275,6 +278,10 @@ object IFeatureCollection.this[Type key]
275278
{
276279
feature = _currentIHttpResponseStartFeature;
277280
}
281+
else if (key == IHttpResetFeatureType)
282+
{
283+
feature = _currentIHttpResetFeature;
284+
}
278285
else if (key == IHttpSendFileFeatureType)
279286
{
280287
feature = _currentIHttpSendFileFeature;
@@ -403,6 +410,10 @@ object IFeatureCollection.this[Type key]
403410
{
404411
_currentIHttpResponseStartFeature = value;
405412
}
413+
else if (key == IHttpResetFeatureType)
414+
{
415+
_currentIHttpResetFeature = value;
416+
}
406417
else if (key == IHttpSendFileFeatureType)
407418
{
408419
_currentIHttpSendFileFeature = value;
@@ -529,6 +540,10 @@ TFeature IFeatureCollection.Get<TFeature>()
529540
{
530541
feature = (TFeature)_currentIHttpResponseStartFeature;
531542
}
543+
else if (typeof(TFeature) == typeof(IHttpResetFeature))
544+
{
545+
feature = (TFeature)_currentIHttpResetFeature;
546+
}
532547
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
533548
{
534549
feature = (TFeature)_currentIHttpSendFileFeature;
@@ -661,6 +676,10 @@ void IFeatureCollection.Set<TFeature>(TFeature feature)
661676
{
662677
_currentIHttpResponseStartFeature = feature;
663678
}
679+
else if (typeof(TFeature) == typeof(IHttpResetFeature))
680+
{
681+
_currentIHttpResetFeature = feature;
682+
}
664683
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
665684
{
666685
_currentIHttpSendFileFeature = feature;
@@ -785,6 +804,10 @@ private IEnumerable<KeyValuePair<Type, object>> FastEnumerable()
785804
{
786805
yield return new KeyValuePair<Type, object>(IHttpResponseStartFeatureType, _currentIHttpResponseStartFeature);
787806
}
807+
if (_currentIHttpResetFeature != null)
808+
{
809+
yield return new KeyValuePair<Type, object>(IHttpResetFeatureType, _currentIHttpResetFeature);
810+
}
788811
if (_currentIHttpSendFileFeature != null)
789812
{
790813
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature);

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.FeatureCollection.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Connections;
67
using Microsoft.AspNetCore.Http;
78
using Microsoft.AspNetCore.Http.Features;
89
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
@@ -12,6 +13,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
1213
{
1314
internal partial class Http2Stream : IHttp2StreamIdFeature,
1415
IHttpMinRequestBodyDataRateFeature,
16+
IHttpResetFeature,
1517
IHttpResponseCompletionFeature,
1618
IHttpResponseTrailersFeature
1719

@@ -73,5 +75,11 @@ async Task IHttpResponseCompletionFeature.CompleteAsync()
7375
await ProduceEnd();
7476
}
7577
}
78+
79+
void IHttpResetFeature.Reset(int errorCode)
80+
{
81+
var abortReason = new ConnectionAbortedException(CoreStrings.FormatHttp2StreamResetByApplication((Http2ErrorCode)errorCode));
82+
ResetAndAbort(abortReason, (Http2ErrorCode)errorCode);
83+
}
7684
}
7785
}

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3859,6 +3859,50 @@ public async Task AbortedStream_ResetsAndDrainsRequest(int intFinalFrameType)
38593859
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
38603860
}
38613861

3862+
[Theory]
3863+
[InlineData((int)(Http2FrameType.DATA))]
3864+
[InlineData((int)(Http2FrameType.WINDOW_UPDATE))]
3865+
[InlineData((int)(Http2FrameType.HEADERS))]
3866+
[InlineData((int)(Http2FrameType.CONTINUATION))]
3867+
public async Task ResetStream_ResetsAndDrainsRequest(int intFinalFrameType)
3868+
{
3869+
var finalFrameType = (Http2FrameType)intFinalFrameType;
3870+
3871+
var headers = new[]
3872+
{
3873+
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
3874+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
3875+
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
3876+
};
3877+
await InitializeConnectionAsync(_appReset);
3878+
3879+
await StartStreamAsync(1, headers, endStream: false);
3880+
3881+
await WaitForStreamErrorAsync(1, Http2ErrorCode.CANCEL, "The HTTP/2 stream was reset by the application with error code CANCEL.");
3882+
3883+
// These would be refused if the cool-down period had expired
3884+
switch (finalFrameType)
3885+
{
3886+
case Http2FrameType.DATA:
3887+
await SendDataAsync(1, new byte[100], endStream: true);
3888+
break;
3889+
case Http2FrameType.WINDOW_UPDATE:
3890+
await SendWindowUpdateAsync(1, 1024);
3891+
break;
3892+
case Http2FrameType.HEADERS:
3893+
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM | Http2HeadersFrameFlags.END_HEADERS, _requestTrailers);
3894+
break;
3895+
case Http2FrameType.CONTINUATION:
3896+
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM, _requestTrailers);
3897+
await SendContinuationAsync(1, Http2ContinuationFrameFlags.END_HEADERS, _requestTrailers);
3898+
break;
3899+
default:
3900+
throw new NotImplementedException(finalFrameType.ToString());
3901+
}
3902+
3903+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
3904+
}
3905+
38623906
[Theory]
38633907
[InlineData((int)(Http2FrameType.DATA))]
38643908
[InlineData((int)(Http2FrameType.HEADERS))]

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4107,5 +4107,178 @@ await InitializeConnectionAsync(async context =>
41074107
Assert.Single(_decodedHeaders);
41084108
Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
41094109
}
4110+
4111+
[Fact]
4112+
public async Task ResetAfterCompleteAsync_GETWithResponseBodyAndTrailers_ResetsAfterResponse()
4113+
{
4114+
var startingTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
4115+
var appTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
4116+
var clientTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
4117+
var headers = new[]
4118+
{
4119+
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
4120+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
4121+
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
4122+
};
4123+
await InitializeConnectionAsync(async context =>
4124+
{
4125+
try
4126+
{
4127+
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
4128+
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
4129+
Assert.NotNull(completionFeature);
4130+
4131+
await context.Response.WriteAsync("Hello World");
4132+
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
4133+
Assert.True(context.Response.Headers.IsReadOnly);
4134+
4135+
context.Response.AppendTrailer("CustomName", "Custom Value");
4136+
4137+
await completionFeature.CompleteAsync().DefaultTimeout();
4138+
4139+
Assert.True(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);
4140+
4141+
// RequestAborted will no longer fire after CompleteAsync.
4142+
Assert.False(context.RequestAborted.CanBeCanceled);
4143+
var resetFeature = context.Features.Get<IHttpResetFeature>();
4144+
Assert.NotNull(resetFeature);
4145+
resetFeature.Reset((int)Http2ErrorCode.NO_ERROR);
4146+
4147+
// Make sure the client gets our results from CompleteAsync instead of from the request delegate exiting.
4148+
await clientTcs.Task.DefaultTimeout();
4149+
appTcs.SetResult(0);
4150+
}
4151+
catch (Exception ex)
4152+
{
4153+
appTcs.SetException(ex);
4154+
}
4155+
});
4156+
4157+
await StartStreamAsync(1, headers, endStream: true);
4158+
4159+
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
4160+
withLength: 37,
4161+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
4162+
withStreamId: 1);
4163+
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
4164+
withLength: 11,
4165+
withFlags: (byte)(Http2HeadersFrameFlags.NONE),
4166+
withStreamId: 1);
4167+
var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
4168+
withLength: 25,
4169+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
4170+
withStreamId: 1);
4171+
await WaitForStreamErrorAsync(1, Http2ErrorCode.NO_ERROR, expectedErrorMessage:
4172+
"The HTTP/2 stream was reset by the application with error code NO_ERROR.");
4173+
4174+
clientTcs.SetResult(0);
4175+
await appTcs.Task;
4176+
4177+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
4178+
4179+
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
4180+
4181+
Assert.Equal(2, _decodedHeaders.Count);
4182+
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
4183+
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
4184+
4185+
Assert.Equal("Hello World", Encoding.UTF8.GetString(bodyFrame.Payload.Span));
4186+
4187+
_decodedHeaders.Clear();
4188+
4189+
_hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
4190+
4191+
Assert.Single(_decodedHeaders);
4192+
Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
4193+
}
4194+
4195+
[Fact]
4196+
public async Task ResetAfterCompleteAsync_POSTWithResponseBodyAndTrailers_RequestBodyThrows()
4197+
{
4198+
var startingTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
4199+
var appTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
4200+
var clientTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
4201+
var headers = new[]
4202+
{
4203+
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
4204+
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
4205+
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
4206+
};
4207+
await InitializeConnectionAsync(async context =>
4208+
{
4209+
try
4210+
{
4211+
var requestBodyTask = context.Request.BodyReader.ReadAsync();
4212+
4213+
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
4214+
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
4215+
Assert.NotNull(completionFeature);
4216+
4217+
await context.Response.WriteAsync("Hello World");
4218+
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
4219+
Assert.True(context.Response.Headers.IsReadOnly);
4220+
4221+
context.Response.AppendTrailer("CustomName", "Custom Value");
4222+
4223+
await completionFeature.CompleteAsync().DefaultTimeout();
4224+
4225+
Assert.True(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);
4226+
4227+
// RequestAborted will no longer fire after CompleteAsync.
4228+
Assert.False(context.RequestAborted.CanBeCanceled);
4229+
var resetFeature = context.Features.Get<IHttpResetFeature>();
4230+
Assert.NotNull(resetFeature);
4231+
resetFeature.Reset((int)Http2ErrorCode.NO_ERROR);
4232+
4233+
await Assert.ThrowsAsync<TaskCanceledException>(async () => await requestBodyTask);
4234+
await Assert.ThrowsAsync<ConnectionAbortedException>(async () => await context.Request.BodyReader.ReadAsync());
4235+
4236+
// Make sure the client gets our results from CompleteAsync instead of from the request delegate exiting.
4237+
await clientTcs.Task.DefaultTimeout();
4238+
appTcs.SetResult(0);
4239+
}
4240+
catch (Exception ex)
4241+
{
4242+
appTcs.SetException(ex);
4243+
}
4244+
});
4245+
4246+
await StartStreamAsync(1, headers, endStream: false);
4247+
4248+
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
4249+
withLength: 37,
4250+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
4251+
withStreamId: 1);
4252+
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
4253+
withLength: 11,
4254+
withFlags: (byte)(Http2HeadersFrameFlags.NONE),
4255+
withStreamId: 1);
4256+
var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
4257+
withLength: 25,
4258+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
4259+
withStreamId: 1);
4260+
await WaitForStreamErrorAsync(1, Http2ErrorCode.NO_ERROR, expectedErrorMessage:
4261+
"The HTTP/2 stream was reset by the application with error code NO_ERROR.");
4262+
4263+
clientTcs.SetResult(0);
4264+
await appTcs.Task;
4265+
4266+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
4267+
4268+
_hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
4269+
4270+
Assert.Equal(2, _decodedHeaders.Count);
4271+
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
4272+
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
4273+
4274+
Assert.Equal("Hello World", Encoding.UTF8.GetString(bodyFrame.Payload.Span));
4275+
4276+
_decodedHeaders.Clear();
4277+
4278+
_hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
4279+
4280+
Assert.Single(_decodedHeaders);
4281+
Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
4282+
}
41104283
}
41114284
}

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ protected static IEnumerable<KeyValuePair<string, string>> ReadRateRequestHeader
151151
protected readonly RequestDelegate _echoHost;
152152
protected readonly RequestDelegate _echoPath;
153153
protected readonly RequestDelegate _appAbort;
154+
protected readonly RequestDelegate _appReset;
154155

155156
internal TestServiceContext _serviceContext;
156157
private Timer _timer;
@@ -390,6 +391,14 @@ public Http2TestBase()
390391
context.Abort();
391392
return Task.CompletedTask;
392393
};
394+
395+
_appReset = context =>
396+
{
397+
var resetFeature = context.Features.Get<IHttpResetFeature>();
398+
Assert.NotNull(resetFeature);
399+
resetFeature.Reset((int)Http2ErrorCode.CANCEL);
400+
return Task.CompletedTask;
401+
};
393402
}
394403

395404
public override void Initialize(MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper)

0 commit comments

Comments
 (0)