Add StartAsync() to HttpResponse; ImplementFeature in Kestrel (#6967)

This commit is contained in:
Justin Kotalik 2019-01-28 17:42:04 -08:00 committed by GitHub
parent e1971f1d12
commit 1222d8de49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 913 additions and 33 deletions

View File

@ -4,7 +4,9 @@
using System;
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Http
{
@ -111,5 +113,14 @@ namespace Microsoft.AspNetCore.Http
/// where only ASCII characters are allowed.</param>
/// <param name="permanent"><c>True</c> if the redirect is permanent (301), otherwise <c>false</c> (302).</param>
public abstract void Redirect(string location, bool permanent);
/// <summary>
/// Starts the response by calling OnStarting() and making headers unmodifiable.
/// </summary>
/// <param name="cancellationToken"></param>
/// <remarks>
/// If the <see cref="IHttpResponseStartFeature"/> isn't set, StartAsync will default to calling HttpResponse.Body.FlushAsync().
/// </remarks>
public abstract Task StartAsync(CancellationToken cancellationToken = default);
}
}

View File

@ -0,0 +1,19 @@
// 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.
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Http.Features
{
/// <summary>
/// Feature to start response writing.
/// </summary>
public interface IHttpResponseStartFeature
{
/// <summary>
/// Starts the response by calling OnStarting() and making headers unmodifiable.
/// </summary>
Task StartAsync(CancellationToken token = default);
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Net.Http.Headers;
@ -14,6 +15,7 @@ namespace Microsoft.AspNetCore.Http.Internal
{
// Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624
private readonly static Func<IFeatureCollection, IHttpResponseFeature> _nullResponseFeature = f => null;
private readonly static Func<IFeatureCollection, IHttpResponseStartFeature> _nullResponseStartFeature = f => null;
private readonly static Func<IFeatureCollection, IResponseCookiesFeature> _newResponseCookiesFeature = f => new ResponseCookiesFeature(f);
private readonly static Func<HttpContext, IResponseBodyPipeFeature> _newResponseBodyPipeFeature = context => new ResponseBodyPipeFeature(context);
@ -39,6 +41,9 @@ namespace Microsoft.AspNetCore.Http.Internal
private IHttpResponseFeature HttpResponseFeature =>
_features.Fetch(ref _features.Cache.Response, _nullResponseFeature);
private IHttpResponseStartFeature HttpResponseStartFeature =>
_features.Fetch(ref _features.Cache.ResponseStart, _nullResponseStartFeature);
private IResponseCookiesFeature ResponseCookiesFeature =>
_features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature);
@ -139,11 +144,22 @@ namespace Microsoft.AspNetCore.Http.Internal
Headers[HeaderNames.Location] = location;
}
public override Task StartAsync(CancellationToken cancellationToken = default)
{
if (HttpResponseStartFeature == null)
{
return HttpResponseFeature.Body.FlushAsync(cancellationToken);
}
return HttpResponseStartFeature.StartAsync();
}
struct FeatureInterfaces
{
public IHttpResponseFeature Response;
public IResponseCookiesFeature Cookies;
public IResponseBodyPipeFeature BodyPipe;
public IHttpResponseStartFeature ResponseStart;
}
}
}

View File

@ -159,15 +159,16 @@ namespace Microsoft.AspNetCore.Http
features.Set<IHttpRequestFeature>(new HttpRequestFeature());
features.Set<IHttpResponseFeature>(new HttpResponseFeature());
features.Set<IHttpWebSocketFeature>(new TestHttpWebSocketFeature());
features.Set<IHttpResponseStartFeature>(new MockHttpResponseStartFeature());
// FeatureCollection is set. all cached interfaces are null.
var context = new DefaultHttpContext(features);
TestAllCachedFeaturesAreNull(context, features);
Assert.Equal(3, features.Count());
Assert.Equal(4, features.Count());
// getting feature properties populates feature collection with defaults
TestAllCachedFeaturesAreSet(context, features);
Assert.NotEqual(3, features.Count());
Assert.NotEqual(4, features.Count());
// FeatureCollection is null. and all cached interfaces are null.
// only top level is tested because child objects are inaccessible.
@ -179,15 +180,16 @@ namespace Microsoft.AspNetCore.Http
newFeatures.Set<IHttpRequestFeature>(new HttpRequestFeature());
newFeatures.Set<IHttpResponseFeature>(new HttpResponseFeature());
newFeatures.Set<IHttpWebSocketFeature>(new TestHttpWebSocketFeature());
newFeatures.Set<IHttpResponseStartFeature>(new MockHttpResponseStartFeature());
// FeatureCollection is set to newFeatures. all cached interfaces are null.
context.Initialize(newFeatures);
TestAllCachedFeaturesAreNull(context, newFeatures);
Assert.Equal(3, newFeatures.Count());
Assert.Equal(4, newFeatures.Count());
// getting feature properties populates new feature collection with defaults
TestAllCachedFeaturesAreSet(context, newFeatures);
Assert.NotEqual(3, newFeatures.Count());
Assert.NotEqual(4, newFeatures.Count());
}
[Fact]
@ -417,5 +419,13 @@ namespace Microsoft.AspNetCore.Http
throw new NotImplementedException();
}
}
private class MockHttpResponseStartFeature : IHttpResponseStartFeature
{
public Task StartAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -6,8 +6,11 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Http.Internal
@ -99,6 +102,32 @@ namespace Microsoft.AspNetCore.Http.Internal
Assert.Throws<ArgumentNullException>(() => context.Response.BodyPipe = null);
}
[Fact]
public async Task ResponseStart_CallsFeatureIfSet()
{
var features = new FeatureCollection();
var mock = new Mock<IHttpResponseStartFeature>();
mock.Setup(o => o.StartAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
features.Set(mock.Object);
var context = new DefaultHttpContext(features);
await context.Response.StartAsync();
mock.Verify(m => m.StartAsync(default), Times.Once());
}
[Fact]
public async Task ResponseStart_CallsResponseBodyFlushIfNotSet()
{
var context = new DefaultHttpContext();
var mock = new FlushAsyncCheckStream();
context.Response.Body = mock;
await context.Response.StartAsync(default);
Assert.True(mock.IsCalled);
}
private static HttpResponse CreateResponse(IHeaderDictionary headers)
{
var context = new DefaultHttpContext();
@ -126,5 +155,16 @@ namespace Microsoft.AspNetCore.Http.Internal
return CreateResponse(headers);
}
private class FlushAsyncCheckStream : MemoryStream
{
public bool IsCalled { get; private set; }
public override Task FlushAsync(CancellationToken cancellationToken)
{
IsCalled = true;
return base.FlushAsync(cancellationToken);
}
}
}
}

View File

@ -20,7 +20,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
IHttpRequestLifetimeFeature,
IHttpRequestIdentifierFeature,
IHttpBodyControlFeature,
IHttpMaxRequestBodySizeFeature
IHttpMaxRequestBodySizeFeature,
IHttpResponseStartFeature
{
// NOTE: When feature interfaces are added to or removed from this HttpProtocol class implementation,
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
@ -250,5 +251,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
protected abstract void ApplicationAbort();
Task IHttpResponseStartFeature.StartAsync(CancellationToken cancellationToken)
{
if (HasResponseStarted)
{
return Task.CompletedTask;
}
return InitializeResponseAsync(0);
}
}
}

View File

@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly Type IHttpMinRequestBodyDataRateFeatureType = typeof(IHttpMinRequestBodyDataRateFeature);
private static readonly Type IHttpMinResponseDataRateFeatureType = typeof(IHttpMinResponseDataRateFeature);
private static readonly Type IHttpBodyControlFeatureType = typeof(IHttpBodyControlFeature);
private static readonly Type IHttpResponseStartFeatureType = typeof(IHttpResponseStartFeature);
private static readonly Type IHttpSendFileFeatureType = typeof(IHttpSendFileFeature);
private object _currentIHttpRequestFeature;
@ -57,6 +58,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private object _currentIHttpMinRequestBodyDataRateFeature;
private object _currentIHttpMinResponseDataRateFeature;
private object _currentIHttpBodyControlFeature;
private object _currentIHttpResponseStartFeature;
private object _currentIHttpSendFileFeature;
private int _featureRevision;
@ -73,6 +75,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpConnectionFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this;
_currentIHttpBodyControlFeature = this;
_currentIHttpResponseStartFeature = this;
_currentIServiceProvidersFeature = null;
_currentIHttpAuthenticationFeature = null;
@ -226,6 +229,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = _currentIHttpBodyControlFeature;
}
else if (key == IHttpResponseStartFeatureType)
{
feature = _currentIHttpResponseStartFeature;
}
else if (key == IHttpSendFileFeatureType)
{
feature = _currentIHttpSendFileFeature;
@ -326,6 +333,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttpBodyControlFeature = value;
}
else if (key == IHttpResponseStartFeatureType)
{
_currentIHttpResponseStartFeature = value;
}
else if (key == IHttpSendFileFeatureType)
{
_currentIHttpSendFileFeature = value;
@ -424,6 +435,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = (TFeature)_currentIHttpBodyControlFeature;
}
else if (typeof(TFeature) == typeof(IHttpResponseStartFeature))
{
feature = (TFeature)_currentIHttpResponseStartFeature;
}
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
{
feature = (TFeature)_currentIHttpSendFileFeature;
@ -528,6 +543,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttpBodyControlFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpResponseStartFeature))
{
_currentIHttpResponseStartFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
{
_currentIHttpSendFileFeature = feature;
@ -624,6 +643,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
yield return new KeyValuePair<Type, object>(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature);
}
if (_currentIHttpResponseStartFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpResponseStartFeatureType, _currentIHttpResponseStartFeature);
}
if (_currentIHttpSendFileFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature);

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected volatile bool _keepAlive = true;
private bool _canHaveBody;
private bool _autoChunk;
private Exception _applicationException;
protected Exception _applicationException;
private BadHttpRequestException _requestRejectedException;
protected HttpVersion _httpVersion;
@ -266,7 +266,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
public bool HasResponseStarted => _requestProcessingStatus == RequestProcessingStatus.ResponseStarted;
public bool HasResponseStarted => _requestProcessingStatus >= RequestProcessingStatus.HeadersCommitted;
public bool HasFlushedHeaders => _requestProcessingStatus == RequestProcessingStatus.HeadersFlushed;
protected HttpRequestHeaders HttpRequestHeaders { get; } = new HttpRequestHeaders();
@ -786,18 +788,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// If return is Task.CompletedTask no awaiting is required
if (!ReferenceEquals(initializeTask, Task.CompletedTask))
{
return FlushAsyncAwaited(initializeTask, cancellationToken);
return InitializeAndFlushAsyncAwaited(initializeTask, cancellationToken);
}
}
return Output.FlushAsync(cancellationToken);
return FlushAsyncInternal(cancellationToken);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private async Task FlushAsyncAwaited(Task initializeTask, CancellationToken cancellationToken)
private async Task InitializeAndFlushAsyncAwaited(Task initializeTask, CancellationToken cancellationToken)
{
await initializeTask;
await Output.FlushAsync(cancellationToken);
await FlushAsyncInternal(cancellationToken);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private Task FlushAsyncInternal(CancellationToken cancellationToken)
{
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
return Output.FlushAsync(cancellationToken);
}
public Task WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default(CancellationToken))
@ -825,20 +834,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
if (data.Length == 0)
{
return !firstWrite ? Task.CompletedTask : FlushAsync(cancellationToken);
return !firstWrite ? Task.CompletedTask : FlushAsyncInternal(cancellationToken);
}
return WriteChunkedAsync(data.Span, cancellationToken);
}
else
{
CheckLastWrite();
return Output.WriteDataAsync(data.Span, cancellationToken: cancellationToken);
return WriteDataAsync(data, cancellationToken: cancellationToken);
}
}
else
{
HandleNonBodyResponseWrite();
return !firstWrite ? Task.CompletedTask : FlushAsync(cancellationToken);
return !firstWrite ? Task.CompletedTask : FlushAsyncInternal(cancellationToken);
}
}
@ -854,7 +863,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
if (data.Length == 0)
{
await FlushAsync(cancellationToken);
await FlushAsyncInternal(cancellationToken);
return;
}
@ -863,16 +872,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
else
{
CheckLastWrite();
await Output.WriteDataAsync(data.Span, cancellationToken: cancellationToken);
await WriteDataAsync(data, cancellationToken);
}
}
else
{
HandleNonBodyResponseWrite();
await FlushAsync(cancellationToken);
await FlushAsyncInternal(cancellationToken);
}
}
private Task WriteDataAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
{
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
return Output.WriteDataAsync(data.Span, cancellationToken: cancellationToken);
}
private void VerifyAndUpdateWrite(int count)
{
var responseHeaders = HttpResponseHeaders;
@ -945,6 +960,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private Task WriteChunkedAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken)
{
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
return Output.WriteChunkAsync(data, cancellationToken);
}
@ -1004,7 +1020,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return;
}
_requestProcessingStatus = RequestProcessingStatus.ResponseStarted;
_requestProcessingStatus = RequestProcessingStatus.HeadersCommitted;
CreateResponseHeader(appCompleted);
}
@ -1047,23 +1063,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
if (!HasResponseStarted)
{
return ProduceEndAwaited();
ProduceStart(appCompleted: true);
}
return WriteSuffix();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private async Task ProduceEndAwaited()
{
ProduceStart(appCompleted: true);
// Force flush
await Output.FlushAsync(default(CancellationToken));
await WriteSuffix();
}
private Task WriteSuffix()
{
// _autoChunk should be checked after we are sure ProduceStart() has been called
@ -1083,6 +1088,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
Log.ConnectionHeadResponseBodyWrite(ConnectionId, _responseBytesWritten);
}
if (!HasFlushedHeaders)
{
return FlushAsyncInternal(default);
}
return Task.CompletedTask;
}
@ -1091,6 +1101,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// For the same reason we call CheckLastWrite() in Content-Length responses.
PreventRequestAbortedCancellation();
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
await Output.WriteStreamSuffixAsync();
if (_keepAlive)

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.Server.Kestrel.Core.Internal.Http
@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
ParsingRequestLine,
ParsingHeaders,
AppStarted,
ResponseStarted
HeadersCommitted,
HeadersFlushed
}
}
}

View File

@ -124,6 +124,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_collection[typeof(IHttpMinRequestBodyDataRateFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpMinResponseDataRateFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpBodyControlFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpResponseStartFeature)] = CreateHttp1Connection();
CompareGenericGetterToIndexer();
@ -142,6 +143,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_collection.Set<IHttpMinRequestBodyDataRateFeature>(CreateHttp1Connection());
_collection.Set<IHttpMinResponseDataRateFeature>(CreateHttp1Connection());
_collection.Set<IHttpBodyControlFeature>(CreateHttp1Connection());
_collection.Set<IHttpResponseStartFeature>(CreateHttp1Connection());
CompareGenericGetterToIndexer();
@ -178,6 +180,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Same(_collection.Get<IHttpMinRequestBodyDataRateFeature>(), _collection[typeof(IHttpMinRequestBodyDataRateFeature)]);
Assert.Same(_collection.Get<IHttpMinResponseDataRateFeature>(), _collection[typeof(IHttpMinResponseDataRateFeature)]);
Assert.Same(_collection.Get<IHttpBodyControlFeature>(), _collection[typeof(IHttpBodyControlFeature)]);
Assert.Same(_collection.Get<IHttpResponseStartFeature>(), _collection[typeof(IHttpResponseStartFeature)]);
}
private int EachHttpProtocolFeatureSetAndUnique()

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -1024,6 +1025,388 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
}
[Fact]
public async Task StartAsync_Response_NoBytesWritten_Sends200()
{
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.StartAsync();
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
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(2, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
}
[Fact]
public async Task StartAsync_ContentLength_Response_NoBytesWritten_Sends200()
{
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 =>
{
context.Response.ContentLength = 0;
await context.Response.StartAsync();
});
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 StartAsync_OnStartingThrowsAfterStartAsyncIsCalled()
{
InvalidOperationException ex = null;
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.StartAsync();
ex = Assert.Throws<InvalidOperationException>(() => context.Response.OnStarting(_ => Task.CompletedTask, null));
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
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);
Assert.NotNull(ex);
_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]);
}
[Fact]
public async Task StartAsync_StartsResponse()
{
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.StartAsync();
Assert.True(context.Response.HasStarted);
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
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(2, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
}
[Fact]
public async Task StartAsync_WithoutFinalFlushDoesNotFlushUntilResponseEnd()
{
var tcs = new TaskCompletionSource<object>(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.StartAsync();
// Verify that the response isn't flushed by verifying the TCS isn't set
var res = await Task.WhenAny(tcs.Task, Task.Delay(1000)) == tcs.Task;
await context.Response.WriteAsync("hello, world");
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
withLength: 12,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
tcs.SetResult(null);
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(2, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray()));
}
[Fact]
public async Task StartAsync_FlushStillFlushesBody()
{
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.StartAsync();
// Verify that the response isn't flushed by verifying the TCS isn't set
await context.Response.Body.FlushAsync();
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
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(2, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
}
[Fact]
public async Task StartAsync_WithContentLengthAndEmptyWriteCallsFinalFlush()
{
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 =>
{
context.Response.ContentLength = 0;
await context.Response.StartAsync();
await context.Response.WriteAsync("");
});
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 StartAsync_SingleWriteCallsFinalFlush()
{
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.StartAsync();
await context.Response.WriteAsync("hello, world");
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
withLength: 12,
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);
_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.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray()));
}
[Fact]
public async Task StartAsync_ContentLength_ThrowsException_DataIsFlushed_ConnectionReset()
{
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 =>
{
context.Response.ContentLength = 11;
await context.Response.StartAsync();
throw new Exception();
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 56,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, "");
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("11", _decodedHeaders[HeaderNames.ContentLength]);
}
[Fact]
public async Task StartAsync_ThrowsException_DataIsFlushed()
{
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.StartAsync();
throw new Exception();
});
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, "");
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]);
}
[Fact]
public async Task ContentLength_Response_TooFewBytesWritten_Resets()
{

View File

@ -9,6 +9,7 @@ using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
@ -102,6 +103,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task OnStartingThrowsWhenSetAfterStartAsyncIsCalled()
{
InvalidOperationException ex = null;
using (var server = new TestServer(async context =>
{
await context.Response.StartAsync();
ex = Assert.Throws<InvalidOperationException>(() => context.Response.OnStarting(_ => Task.CompletedTask, null));
}, new TestServiceContext(LoggerFactory)))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive($"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"0",
"",
"");
Assert.NotNull(ex);
}
await server.StopAsync();
}
}
[Fact]
public async Task ResponseBodyWriteAsyncCanBeCancelled()
{
@ -2336,6 +2370,321 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
[Fact]
public async Task StartAsyncDefaultToChunkedResponse()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.StartAsync();
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task StartAsyncWithContentLengthAndEmptyWriteStillCallsFinalFlush()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.ContentLength = 0;
await httpContext.Response.StartAsync();
await httpContext.Response.WriteAsync("");
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 0",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task StartAsyncAndEmptyWriteStillCallsFinalFlush()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.StartAsync();
await httpContext.Response.WriteAsync("");
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task StartAsyncWithSingleChunkedWriteStillWritesSuffix()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.StartAsync();
await httpContext.Response.WriteAsync("Hello World!");
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"c",
"Hello World!",
"0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task StartAsyncWithoutFlushStartsResponse()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.StartAsync();
Assert.True(httpContext.Response.HasStarted);
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task StartAsyncThrowExceptionThrowsConnectionAbortedException()
{
var testContext = new TestServiceContext(LoggerFactory);
var expectedException = new Exception();
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.StartAsync();
throw expectedException;
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task StartAsyncWithContentLengthThrowExceptionThrowsConnectionAbortedException()
{
var testContext = new TestServiceContext(LoggerFactory);
var expectedException = new Exception();
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.ContentLength = 11;
await httpContext.Response.StartAsync();
throw expectedException;
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 11",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task StartAsyncWithoutFlushingDoesNotFlush()
{
var testContext = new TestServiceContext(LoggerFactory);
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.StartAsync();
Assert.True(httpContext.Response.HasStarted);
// Verify that the response isn't flushed by verifying the TCS isn't set
var res = await Task.WhenAny(tcs.Task, Task.Delay(1000)) == tcs.Task;
Assert.False(res);
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"0",
"",
"");
// If we reach this point before the app exits, this means the flush finished early.
tcs.SetResult(null);
}
await server.StopAsync();
}
}
[Fact]
public async Task StartAsyncWithContentLengthWritingWorks()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.Headers["Content-Length"] = new[] { "11" };
await httpContext.Response.StartAsync();
await httpContext.Response.WriteAsync("Hello World");
Assert.True(httpContext.Response.HasStarted);
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 11",
"",
"Hello World");
}
await server.StopAsync();
}
}
[Fact]
public async Task StartAsyncAndFlushWorks()
{
var testContext = new TestServiceContext(LoggerFactory);
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.StartAsync();
await httpContext.Response.Body.FlushAsync();
Assert.True(httpContext.Response.HasStarted);
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"Transfer-Encoding: chunked",
"",
"0",
"",
"");
}
await server.StopAsync();
}
}
[Fact]
public async Task OnStartingCallbacksAreCalledInLastInFirstOutOrder()
{

View File

@ -40,6 +40,7 @@ namespace CodeGenerator
"IHttpMinRequestBodyDataRateFeature",
"IHttpMinResponseDataRateFeature",
"IHttpBodyControlFeature",
"IHttpResponseStartFeature"
};
var rareFeatures = new[]
@ -65,6 +66,7 @@ namespace CodeGenerator
"IHttpConnectionFeature",
"IHttpMaxRequestBodySizeFeature",
"IHttpBodyControlFeature",
"IHttpResponseStartFeature"
};
var usings = $@"