parent
8984144db4
commit
9077b38805
|
|
@ -200,6 +200,10 @@ namespace Microsoft.AspNetCore.Http.Features
|
||||||
bool Available { get; }
|
bool Available { get; }
|
||||||
Microsoft.AspNetCore.Http.IHeaderDictionary Trailers { get; }
|
Microsoft.AspNetCore.Http.IHeaderDictionary Trailers { get; }
|
||||||
}
|
}
|
||||||
|
public partial interface IHttpResetFeature
|
||||||
|
{
|
||||||
|
void Reset(int errorCode);
|
||||||
|
}
|
||||||
public partial interface IHttpResponseCompletionFeature
|
public partial interface IHttpResponseCompletionFeature
|
||||||
{
|
{
|
||||||
System.Threading.Tasks.Task CompleteAsync();
|
System.Threading.Tasks.Task CompleteAsync();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Http.Features
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to send reset messages for protocols that support them such as HTTP/2 or HTTP/3.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHttpResetFeature
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Send a reset message with the given error code. The request will be considered aborted.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="errorCode">The error code to send in the reset message.</param>
|
||||||
|
void Reset(int errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -608,4 +608,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
|
||||||
<data name="HTTP2NoTlsOsx" xml:space="preserve">
|
<data name="HTTP2NoTlsOsx" xml:space="preserve">
|
||||||
<value>HTTP/2 over TLS is not supported on OSX due to missing ALPN support.</value>
|
<value>HTTP/2 over TLS is not supported on OSX due to missing ALPN support.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Http2StreamResetByApplication" xml:space="preserve">
|
||||||
|
<value>The HTTP/2 stream was reset by the application with error code {errorCode}.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -279,6 +279,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
_currentIHttp2StreamIdFeature = this;
|
_currentIHttp2StreamIdFeature = this;
|
||||||
_currentIHttpResponseCompletionFeature = this;
|
_currentIHttpResponseCompletionFeature = this;
|
||||||
_currentIHttpResponseTrailersFeature = this;
|
_currentIHttpResponseTrailersFeature = this;
|
||||||
|
_currentIHttpResetFeature = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state)
|
void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state)
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
private static readonly Type IHttpMinResponseDataRateFeatureType = typeof(IHttpMinResponseDataRateFeature);
|
private static readonly Type IHttpMinResponseDataRateFeatureType = typeof(IHttpMinResponseDataRateFeature);
|
||||||
private static readonly Type IHttpBodyControlFeatureType = typeof(IHttpBodyControlFeature);
|
private static readonly Type IHttpBodyControlFeatureType = typeof(IHttpBodyControlFeature);
|
||||||
private static readonly Type IHttpResponseStartFeatureType = typeof(IHttpResponseStartFeature);
|
private static readonly Type IHttpResponseStartFeatureType = typeof(IHttpResponseStartFeature);
|
||||||
|
private static readonly Type IHttpResetFeatureType = typeof(IHttpResetFeature);
|
||||||
private static readonly Type IHttpSendFileFeatureType = typeof(IHttpSendFileFeature);
|
private static readonly Type IHttpSendFileFeatureType = typeof(IHttpSendFileFeature);
|
||||||
|
|
||||||
private object _currentIHttpRequestFeature;
|
private object _currentIHttpRequestFeature;
|
||||||
|
|
@ -71,6 +72,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
private object _currentIHttpMinResponseDataRateFeature;
|
private object _currentIHttpMinResponseDataRateFeature;
|
||||||
private object _currentIHttpBodyControlFeature;
|
private object _currentIHttpBodyControlFeature;
|
||||||
private object _currentIHttpResponseStartFeature;
|
private object _currentIHttpResponseStartFeature;
|
||||||
|
private object _currentIHttpResetFeature;
|
||||||
private object _currentIHttpSendFileFeature;
|
private object _currentIHttpSendFileFeature;
|
||||||
|
|
||||||
private int _featureRevision;
|
private int _featureRevision;
|
||||||
|
|
@ -108,6 +110,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
_currentIHttpWebSocketFeature = null;
|
_currentIHttpWebSocketFeature = null;
|
||||||
_currentISessionFeature = null;
|
_currentISessionFeature = null;
|
||||||
_currentIHttpMinResponseDataRateFeature = null;
|
_currentIHttpMinResponseDataRateFeature = null;
|
||||||
|
_currentIHttpResetFeature = null;
|
||||||
_currentIHttpSendFileFeature = null;
|
_currentIHttpSendFileFeature = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -275,6 +278,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
feature = _currentIHttpResponseStartFeature;
|
feature = _currentIHttpResponseStartFeature;
|
||||||
}
|
}
|
||||||
|
else if (key == IHttpResetFeatureType)
|
||||||
|
{
|
||||||
|
feature = _currentIHttpResetFeature;
|
||||||
|
}
|
||||||
else if (key == IHttpSendFileFeatureType)
|
else if (key == IHttpSendFileFeatureType)
|
||||||
{
|
{
|
||||||
feature = _currentIHttpSendFileFeature;
|
feature = _currentIHttpSendFileFeature;
|
||||||
|
|
@ -403,6 +410,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
_currentIHttpResponseStartFeature = value;
|
_currentIHttpResponseStartFeature = value;
|
||||||
}
|
}
|
||||||
|
else if (key == IHttpResetFeatureType)
|
||||||
|
{
|
||||||
|
_currentIHttpResetFeature = value;
|
||||||
|
}
|
||||||
else if (key == IHttpSendFileFeatureType)
|
else if (key == IHttpSendFileFeatureType)
|
||||||
{
|
{
|
||||||
_currentIHttpSendFileFeature = value;
|
_currentIHttpSendFileFeature = value;
|
||||||
|
|
@ -529,6 +540,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
feature = (TFeature)_currentIHttpResponseStartFeature;
|
feature = (TFeature)_currentIHttpResponseStartFeature;
|
||||||
}
|
}
|
||||||
|
else if (typeof(TFeature) == typeof(IHttpResetFeature))
|
||||||
|
{
|
||||||
|
feature = (TFeature)_currentIHttpResetFeature;
|
||||||
|
}
|
||||||
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
|
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
|
||||||
{
|
{
|
||||||
feature = (TFeature)_currentIHttpSendFileFeature;
|
feature = (TFeature)_currentIHttpSendFileFeature;
|
||||||
|
|
@ -661,6 +676,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
_currentIHttpResponseStartFeature = feature;
|
_currentIHttpResponseStartFeature = feature;
|
||||||
}
|
}
|
||||||
|
else if (typeof(TFeature) == typeof(IHttpResetFeature))
|
||||||
|
{
|
||||||
|
_currentIHttpResetFeature = feature;
|
||||||
|
}
|
||||||
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
|
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
|
||||||
{
|
{
|
||||||
_currentIHttpSendFileFeature = feature;
|
_currentIHttpSendFileFeature = feature;
|
||||||
|
|
@ -785,6 +804,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
yield return new KeyValuePair<Type, object>(IHttpResponseStartFeatureType, _currentIHttpResponseStartFeature);
|
yield return new KeyValuePair<Type, object>(IHttpResponseStartFeatureType, _currentIHttpResponseStartFeature);
|
||||||
}
|
}
|
||||||
|
if (_currentIHttpResetFeature != null)
|
||||||
|
{
|
||||||
|
yield return new KeyValuePair<Type, object>(IHttpResetFeatureType, _currentIHttpResetFeature);
|
||||||
|
}
|
||||||
if (_currentIHttpSendFileFeature != null)
|
if (_currentIHttpSendFileFeature != null)
|
||||||
{
|
{
|
||||||
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature);
|
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
||||||
|
|
@ -12,6 +13,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
{
|
{
|
||||||
internal partial class Http2Stream : IHttp2StreamIdFeature,
|
internal partial class Http2Stream : IHttp2StreamIdFeature,
|
||||||
IHttpMinRequestBodyDataRateFeature,
|
IHttpMinRequestBodyDataRateFeature,
|
||||||
|
IHttpResetFeature,
|
||||||
IHttpResponseCompletionFeature,
|
IHttpResponseCompletionFeature,
|
||||||
IHttpResponseTrailersFeature
|
IHttpResponseTrailersFeature
|
||||||
|
|
||||||
|
|
@ -73,5 +75,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
await ProduceEnd();
|
await ProduceEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IHttpResetFeature.Reset(int errorCode)
|
||||||
|
{
|
||||||
|
var abortReason = new ConnectionAbortedException(CoreStrings.FormatHttp2StreamResetByApplication((Http2ErrorCode)errorCode));
|
||||||
|
ResetAndAbort(abortReason, (Http2ErrorCode)errorCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3859,6 +3859,50 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
|
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData((int)(Http2FrameType.DATA))]
|
||||||
|
[InlineData((int)(Http2FrameType.WINDOW_UPDATE))]
|
||||||
|
[InlineData((int)(Http2FrameType.HEADERS))]
|
||||||
|
[InlineData((int)(Http2FrameType.CONTINUATION))]
|
||||||
|
public async Task ResetStream_ResetsAndDrainsRequest(int intFinalFrameType)
|
||||||
|
{
|
||||||
|
var finalFrameType = (Http2FrameType)intFinalFrameType;
|
||||||
|
|
||||||
|
var headers = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
|
||||||
|
};
|
||||||
|
await InitializeConnectionAsync(_appReset);
|
||||||
|
|
||||||
|
await StartStreamAsync(1, headers, endStream: false);
|
||||||
|
|
||||||
|
await WaitForStreamErrorAsync(1, Http2ErrorCode.CANCEL, "The HTTP/2 stream was reset by the application with error code CANCEL.");
|
||||||
|
|
||||||
|
// These would be refused if the cool-down period had expired
|
||||||
|
switch (finalFrameType)
|
||||||
|
{
|
||||||
|
case Http2FrameType.DATA:
|
||||||
|
await SendDataAsync(1, new byte[100], endStream: true);
|
||||||
|
break;
|
||||||
|
case Http2FrameType.WINDOW_UPDATE:
|
||||||
|
await SendWindowUpdateAsync(1, 1024);
|
||||||
|
break;
|
||||||
|
case Http2FrameType.HEADERS:
|
||||||
|
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM | Http2HeadersFrameFlags.END_HEADERS, _requestTrailers);
|
||||||
|
break;
|
||||||
|
case Http2FrameType.CONTINUATION:
|
||||||
|
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM, _requestTrailers);
|
||||||
|
await SendContinuationAsync(1, Http2ContinuationFrameFlags.END_HEADERS, _requestTrailers);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException(finalFrameType.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData((int)(Http2FrameType.DATA))]
|
[InlineData((int)(Http2FrameType.DATA))]
|
||||||
[InlineData((int)(Http2FrameType.HEADERS))]
|
[InlineData((int)(Http2FrameType.HEADERS))]
|
||||||
|
|
|
||||||
|
|
@ -4107,5 +4107,178 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
Assert.Single(_decodedHeaders);
|
Assert.Single(_decodedHeaders);
|
||||||
Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
|
Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResetAfterCompleteAsync_GETWithResponseBodyAndTrailers_ResetsAfterResponse()
|
||||||
|
{
|
||||||
|
var startingTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var appTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var clientTcs = 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 =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
|
||||||
|
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
|
||||||
|
Assert.NotNull(completionFeature);
|
||||||
|
|
||||||
|
await context.Response.WriteAsync("Hello World");
|
||||||
|
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
|
||||||
|
Assert.True(context.Response.Headers.IsReadOnly);
|
||||||
|
|
||||||
|
context.Response.AppendTrailer("CustomName", "Custom Value");
|
||||||
|
|
||||||
|
await completionFeature.CompleteAsync().DefaultTimeout();
|
||||||
|
|
||||||
|
Assert.True(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);
|
||||||
|
|
||||||
|
// RequestAborted will no longer fire after CompleteAsync.
|
||||||
|
Assert.False(context.RequestAborted.CanBeCanceled);
|
||||||
|
var resetFeature = context.Features.Get<IHttpResetFeature>();
|
||||||
|
Assert.NotNull(resetFeature);
|
||||||
|
resetFeature.Reset((int)Http2ErrorCode.NO_ERROR);
|
||||||
|
|
||||||
|
// Make sure the client gets our results from CompleteAsync instead of from the request delegate exiting.
|
||||||
|
await clientTcs.Task.DefaultTimeout();
|
||||||
|
appTcs.SetResult(0);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
appTcs.SetException(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await StartStreamAsync(1, headers, endStream: true);
|
||||||
|
|
||||||
|
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 37,
|
||||||
|
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
|
||||||
|
withStreamId: 1);
|
||||||
|
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 11,
|
||||||
|
withFlags: (byte)(Http2HeadersFrameFlags.NONE),
|
||||||
|
withStreamId: 1);
|
||||||
|
var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 25,
|
||||||
|
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
|
||||||
|
withStreamId: 1);
|
||||||
|
await WaitForStreamErrorAsync(1, Http2ErrorCode.NO_ERROR, expectedErrorMessage:
|
||||||
|
"The HTTP/2 stream was reset by the application with error code NO_ERROR.");
|
||||||
|
|
||||||
|
clientTcs.SetResult(0);
|
||||||
|
await appTcs.Task;
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
Assert.Equal("Hello World", Encoding.UTF8.GetString(bodyFrame.Payload.Span));
|
||||||
|
|
||||||
|
_decodedHeaders.Clear();
|
||||||
|
|
||||||
|
_hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
|
||||||
|
|
||||||
|
Assert.Single(_decodedHeaders);
|
||||||
|
Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResetAfterCompleteAsync_POSTWithResponseBodyAndTrailers_RequestBodyThrows()
|
||||||
|
{
|
||||||
|
var startingTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var appTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var clientTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var headers = new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||||
|
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
|
||||||
|
};
|
||||||
|
await InitializeConnectionAsync(async context =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var requestBodyTask = context.Request.BodyReader.ReadAsync();
|
||||||
|
|
||||||
|
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
|
||||||
|
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
|
||||||
|
Assert.NotNull(completionFeature);
|
||||||
|
|
||||||
|
await context.Response.WriteAsync("Hello World");
|
||||||
|
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
|
||||||
|
Assert.True(context.Response.Headers.IsReadOnly);
|
||||||
|
|
||||||
|
context.Response.AppendTrailer("CustomName", "Custom Value");
|
||||||
|
|
||||||
|
await completionFeature.CompleteAsync().DefaultTimeout();
|
||||||
|
|
||||||
|
Assert.True(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);
|
||||||
|
|
||||||
|
// RequestAborted will no longer fire after CompleteAsync.
|
||||||
|
Assert.False(context.RequestAborted.CanBeCanceled);
|
||||||
|
var resetFeature = context.Features.Get<IHttpResetFeature>();
|
||||||
|
Assert.NotNull(resetFeature);
|
||||||
|
resetFeature.Reset((int)Http2ErrorCode.NO_ERROR);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<TaskCanceledException>(async () => await requestBodyTask);
|
||||||
|
await Assert.ThrowsAsync<ConnectionAbortedException>(async () => await context.Request.BodyReader.ReadAsync());
|
||||||
|
|
||||||
|
// Make sure the client gets our results from CompleteAsync instead of from the request delegate exiting.
|
||||||
|
await clientTcs.Task.DefaultTimeout();
|
||||||
|
appTcs.SetResult(0);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
appTcs.SetException(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await StartStreamAsync(1, headers, endStream: false);
|
||||||
|
|
||||||
|
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 37,
|
||||||
|
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
|
||||||
|
withStreamId: 1);
|
||||||
|
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 11,
|
||||||
|
withFlags: (byte)(Http2HeadersFrameFlags.NONE),
|
||||||
|
withStreamId: 1);
|
||||||
|
var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 25,
|
||||||
|
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
|
||||||
|
withStreamId: 1);
|
||||||
|
await WaitForStreamErrorAsync(1, Http2ErrorCode.NO_ERROR, expectedErrorMessage:
|
||||||
|
"The HTTP/2 stream was reset by the application with error code NO_ERROR.");
|
||||||
|
|
||||||
|
clientTcs.SetResult(0);
|
||||||
|
await appTcs.Task;
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
Assert.Equal("Hello World", Encoding.UTF8.GetString(bodyFrame.Payload.Span));
|
||||||
|
|
||||||
|
_decodedHeaders.Clear();
|
||||||
|
|
||||||
|
_hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
|
||||||
|
|
||||||
|
Assert.Single(_decodedHeaders);
|
||||||
|
Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
protected readonly RequestDelegate _echoHost;
|
protected readonly RequestDelegate _echoHost;
|
||||||
protected readonly RequestDelegate _echoPath;
|
protected readonly RequestDelegate _echoPath;
|
||||||
protected readonly RequestDelegate _appAbort;
|
protected readonly RequestDelegate _appAbort;
|
||||||
|
protected readonly RequestDelegate _appReset;
|
||||||
|
|
||||||
internal TestServiceContext _serviceContext;
|
internal TestServiceContext _serviceContext;
|
||||||
private Timer _timer;
|
private Timer _timer;
|
||||||
|
|
@ -390,6 +391,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
context.Abort();
|
context.Abort();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_appReset = context =>
|
||||||
|
{
|
||||||
|
var resetFeature = context.Features.Get<IHttpResetFeature>();
|
||||||
|
Assert.NotNull(resetFeature);
|
||||||
|
resetFeature.Reset((int)Http2ErrorCode.CANCEL);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Initialize(MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper)
|
public override void Initialize(MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ namespace CodeGenerator
|
||||||
"IHttpMinRequestBodyDataRateFeature",
|
"IHttpMinRequestBodyDataRateFeature",
|
||||||
"IHttpMinResponseDataRateFeature",
|
"IHttpMinResponseDataRateFeature",
|
||||||
"IHttpBodyControlFeature",
|
"IHttpBodyControlFeature",
|
||||||
"IHttpResponseStartFeature"
|
"IHttpResponseStartFeature",
|
||||||
|
"IHttpResetFeature"
|
||||||
};
|
};
|
||||||
|
|
||||||
var rareFeatures = new[]
|
var rareFeatures = new[]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue