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;
using System.IO; using System.IO;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Http namespace Microsoft.AspNetCore.Http
{ {
@ -111,5 +113,14 @@ namespace Microsoft.AspNetCore.Http
/// where only ASCII characters are allowed.</param> /// 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> /// <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); 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;
using System.IO; using System.IO;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.Net.Http.Headers; 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 // 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, 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<IFeatureCollection, IResponseCookiesFeature> _newResponseCookiesFeature = f => new ResponseCookiesFeature(f);
private readonly static Func<HttpContext, IResponseBodyPipeFeature> _newResponseBodyPipeFeature = context => new ResponseBodyPipeFeature(context); private readonly static Func<HttpContext, IResponseBodyPipeFeature> _newResponseBodyPipeFeature = context => new ResponseBodyPipeFeature(context);
@ -39,6 +41,9 @@ namespace Microsoft.AspNetCore.Http.Internal
private IHttpResponseFeature HttpResponseFeature => private IHttpResponseFeature HttpResponseFeature =>
_features.Fetch(ref _features.Cache.Response, _nullResponseFeature); _features.Fetch(ref _features.Cache.Response, _nullResponseFeature);
private IHttpResponseStartFeature HttpResponseStartFeature =>
_features.Fetch(ref _features.Cache.ResponseStart, _nullResponseStartFeature);
private IResponseCookiesFeature ResponseCookiesFeature => private IResponseCookiesFeature ResponseCookiesFeature =>
_features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature); _features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature);
@ -139,11 +144,22 @@ namespace Microsoft.AspNetCore.Http.Internal
Headers[HeaderNames.Location] = location; Headers[HeaderNames.Location] = location;
} }
public override Task StartAsync(CancellationToken cancellationToken = default)
{
if (HttpResponseStartFeature == null)
{
return HttpResponseFeature.Body.FlushAsync(cancellationToken);
}
return HttpResponseStartFeature.StartAsync();
}
struct FeatureInterfaces struct FeatureInterfaces
{ {
public IHttpResponseFeature Response; public IHttpResponseFeature Response;
public IResponseCookiesFeature Cookies; public IResponseCookiesFeature Cookies;
public IResponseBodyPipeFeature BodyPipe; public IResponseBodyPipeFeature BodyPipe;
public IHttpResponseStartFeature ResponseStart;
} }
} }
} }

View File

@ -159,15 +159,16 @@ namespace Microsoft.AspNetCore.Http
features.Set<IHttpRequestFeature>(new HttpRequestFeature()); features.Set<IHttpRequestFeature>(new HttpRequestFeature());
features.Set<IHttpResponseFeature>(new HttpResponseFeature()); features.Set<IHttpResponseFeature>(new HttpResponseFeature());
features.Set<IHttpWebSocketFeature>(new TestHttpWebSocketFeature()); features.Set<IHttpWebSocketFeature>(new TestHttpWebSocketFeature());
features.Set<IHttpResponseStartFeature>(new MockHttpResponseStartFeature());
// FeatureCollection is set. all cached interfaces are null. // FeatureCollection is set. all cached interfaces are null.
var context = new DefaultHttpContext(features); var context = new DefaultHttpContext(features);
TestAllCachedFeaturesAreNull(context, features); TestAllCachedFeaturesAreNull(context, features);
Assert.Equal(3, features.Count()); Assert.Equal(4, features.Count());
// getting feature properties populates feature collection with defaults // getting feature properties populates feature collection with defaults
TestAllCachedFeaturesAreSet(context, features); TestAllCachedFeaturesAreSet(context, features);
Assert.NotEqual(3, features.Count()); Assert.NotEqual(4, features.Count());
// FeatureCollection is null. and all cached interfaces are null. // FeatureCollection is null. and all cached interfaces are null.
// only top level is tested because child objects are inaccessible. // 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<IHttpRequestFeature>(new HttpRequestFeature());
newFeatures.Set<IHttpResponseFeature>(new HttpResponseFeature()); newFeatures.Set<IHttpResponseFeature>(new HttpResponseFeature());
newFeatures.Set<IHttpWebSocketFeature>(new TestHttpWebSocketFeature()); newFeatures.Set<IHttpWebSocketFeature>(new TestHttpWebSocketFeature());
newFeatures.Set<IHttpResponseStartFeature>(new MockHttpResponseStartFeature());
// FeatureCollection is set to newFeatures. all cached interfaces are null. // FeatureCollection is set to newFeatures. all cached interfaces are null.
context.Initialize(newFeatures); context.Initialize(newFeatures);
TestAllCachedFeaturesAreNull(context, newFeatures); TestAllCachedFeaturesAreNull(context, newFeatures);
Assert.Equal(3, newFeatures.Count()); Assert.Equal(4, newFeatures.Count());
// getting feature properties populates new feature collection with defaults // getting feature properties populates new feature collection with defaults
TestAllCachedFeaturesAreSet(context, newFeatures); TestAllCachedFeaturesAreSet(context, newFeatures);
Assert.NotEqual(3, newFeatures.Count()); Assert.NotEqual(4, newFeatures.Count());
} }
[Fact] [Fact]
@ -417,5 +419,13 @@ namespace Microsoft.AspNetCore.Http
throw new NotImplementedException(); 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.Globalization;
using System.IO; using System.IO;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Http.Internal namespace Microsoft.AspNetCore.Http.Internal
@ -99,6 +102,32 @@ namespace Microsoft.AspNetCore.Http.Internal
Assert.Throws<ArgumentNullException>(() => context.Response.BodyPipe = null); 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) private static HttpResponse CreateResponse(IHeaderDictionary headers)
{ {
var context = new DefaultHttpContext(); var context = new DefaultHttpContext();
@ -126,5 +155,16 @@ namespace Microsoft.AspNetCore.Http.Internal
return CreateResponse(headers); 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, IHttpRequestLifetimeFeature,
IHttpRequestIdentifierFeature, IHttpRequestIdentifierFeature,
IHttpBodyControlFeature, IHttpBodyControlFeature,
IHttpMaxRequestBodySizeFeature IHttpMaxRequestBodySizeFeature,
IHttpResponseStartFeature
{ {
// NOTE: When feature interfaces are added to or removed from this HttpProtocol class implementation, // 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. // 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(); 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 IHttpMinRequestBodyDataRateFeatureType = typeof(IHttpMinRequestBodyDataRateFeature);
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 IHttpSendFileFeatureType = typeof(IHttpSendFileFeature); private static readonly Type IHttpSendFileFeatureType = typeof(IHttpSendFileFeature);
private object _currentIHttpRequestFeature; private object _currentIHttpRequestFeature;
@ -57,6 +58,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private object _currentIHttpMinRequestBodyDataRateFeature; private object _currentIHttpMinRequestBodyDataRateFeature;
private object _currentIHttpMinResponseDataRateFeature; private object _currentIHttpMinResponseDataRateFeature;
private object _currentIHttpBodyControlFeature; private object _currentIHttpBodyControlFeature;
private object _currentIHttpResponseStartFeature;
private object _currentIHttpSendFileFeature; private object _currentIHttpSendFileFeature;
private int _featureRevision; private int _featureRevision;
@ -73,6 +75,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpConnectionFeature = this; _currentIHttpConnectionFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this; _currentIHttpMaxRequestBodySizeFeature = this;
_currentIHttpBodyControlFeature = this; _currentIHttpBodyControlFeature = this;
_currentIHttpResponseStartFeature = this;
_currentIServiceProvidersFeature = null; _currentIServiceProvidersFeature = null;
_currentIHttpAuthenticationFeature = null; _currentIHttpAuthenticationFeature = null;
@ -226,6 +229,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
feature = _currentIHttpBodyControlFeature; feature = _currentIHttpBodyControlFeature;
} }
else if (key == IHttpResponseStartFeatureType)
{
feature = _currentIHttpResponseStartFeature;
}
else if (key == IHttpSendFileFeatureType) else if (key == IHttpSendFileFeatureType)
{ {
feature = _currentIHttpSendFileFeature; feature = _currentIHttpSendFileFeature;
@ -326,6 +333,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
_currentIHttpBodyControlFeature = value; _currentIHttpBodyControlFeature = value;
} }
else if (key == IHttpResponseStartFeatureType)
{
_currentIHttpResponseStartFeature = value;
}
else if (key == IHttpSendFileFeatureType) else if (key == IHttpSendFileFeatureType)
{ {
_currentIHttpSendFileFeature = value; _currentIHttpSendFileFeature = value;
@ -424,6 +435,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
feature = (TFeature)_currentIHttpBodyControlFeature; feature = (TFeature)_currentIHttpBodyControlFeature;
} }
else if (typeof(TFeature) == typeof(IHttpResponseStartFeature))
{
feature = (TFeature)_currentIHttpResponseStartFeature;
}
else if (typeof(TFeature) == typeof(IHttpSendFileFeature)) else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
{ {
feature = (TFeature)_currentIHttpSendFileFeature; feature = (TFeature)_currentIHttpSendFileFeature;
@ -528,6 +543,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
_currentIHttpBodyControlFeature = feature; _currentIHttpBodyControlFeature = feature;
} }
else if (typeof(TFeature) == typeof(IHttpResponseStartFeature))
{
_currentIHttpResponseStartFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpSendFileFeature)) else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
{ {
_currentIHttpSendFileFeature = feature; _currentIHttpSendFileFeature = feature;
@ -624,6 +643,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
yield return new KeyValuePair<Type, object>(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature); yield return new KeyValuePair<Type, object>(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature);
} }
if (_currentIHttpResponseStartFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpResponseStartFeatureType, _currentIHttpResponseStartFeature);
}
if (_currentIHttpSendFileFeature != null) if (_currentIHttpSendFileFeature != null)
{ {
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature); 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; protected volatile bool _keepAlive = true;
private bool _canHaveBody; private bool _canHaveBody;
private bool _autoChunk; private bool _autoChunk;
private Exception _applicationException; protected Exception _applicationException;
private BadHttpRequestException _requestRejectedException; private BadHttpRequestException _requestRejectedException;
protected HttpVersion _httpVersion; 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(); 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 return is Task.CompletedTask no awaiting is required
if (!ReferenceEquals(initializeTask, Task.CompletedTask)) if (!ReferenceEquals(initializeTask, Task.CompletedTask))
{ {
return FlushAsyncAwaited(initializeTask, cancellationToken); return InitializeAndFlushAsyncAwaited(initializeTask, cancellationToken);
} }
} }
return Output.FlushAsync(cancellationToken); return FlushAsyncInternal(cancellationToken);
} }
[MethodImpl(MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.NoInlining)]
private async Task FlushAsyncAwaited(Task initializeTask, CancellationToken cancellationToken) private async Task InitializeAndFlushAsyncAwaited(Task initializeTask, CancellationToken cancellationToken)
{ {
await initializeTask; 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)) 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) if (data.Length == 0)
{ {
return !firstWrite ? Task.CompletedTask : FlushAsync(cancellationToken); return !firstWrite ? Task.CompletedTask : FlushAsyncInternal(cancellationToken);
} }
return WriteChunkedAsync(data.Span, cancellationToken); return WriteChunkedAsync(data.Span, cancellationToken);
} }
else else
{ {
CheckLastWrite(); CheckLastWrite();
return Output.WriteDataAsync(data.Span, cancellationToken: cancellationToken); return WriteDataAsync(data, cancellationToken: cancellationToken);
} }
} }
else else
{ {
HandleNonBodyResponseWrite(); 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) if (data.Length == 0)
{ {
await FlushAsync(cancellationToken); await FlushAsyncInternal(cancellationToken);
return; return;
} }
@ -863,16 +872,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
else else
{ {
CheckLastWrite(); CheckLastWrite();
await Output.WriteDataAsync(data.Span, cancellationToken: cancellationToken); await WriteDataAsync(data, cancellationToken);
} }
} }
else else
{ {
HandleNonBodyResponseWrite(); 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) private void VerifyAndUpdateWrite(int count)
{ {
var responseHeaders = HttpResponseHeaders; var responseHeaders = HttpResponseHeaders;
@ -945,6 +960,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private Task WriteChunkedAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken) private Task WriteChunkedAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken)
{ {
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
return Output.WriteChunkAsync(data, cancellationToken); return Output.WriteChunkAsync(data, cancellationToken);
} }
@ -1004,7 +1020,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return; return;
} }
_requestProcessingStatus = RequestProcessingStatus.ResponseStarted; _requestProcessingStatus = RequestProcessingStatus.HeadersCommitted;
CreateResponseHeader(appCompleted); CreateResponseHeader(appCompleted);
} }
@ -1047,23 +1063,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
if (!HasResponseStarted) if (!HasResponseStarted)
{ {
return ProduceEndAwaited(); ProduceStart(appCompleted: true);
} }
return WriteSuffix(); return WriteSuffix();
} }
[MethodImpl(MethodImplOptions.NoInlining)]
private async Task ProduceEndAwaited()
{
ProduceStart(appCompleted: true);
// Force flush
await Output.FlushAsync(default(CancellationToken));
await WriteSuffix();
}
private Task WriteSuffix() private Task WriteSuffix()
{ {
// _autoChunk should be checked after we are sure ProduceStart() has been called // _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); Log.ConnectionHeadResponseBodyWrite(ConnectionId, _responseBytesWritten);
} }
if (!HasFlushedHeaders)
{
return FlushAsyncInternal(default);
}
return Task.CompletedTask; 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. // For the same reason we call CheckLastWrite() in Content-Length responses.
PreventRequestAbortedCancellation(); PreventRequestAbortedCancellation();
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
await Output.WriteStreamSuffixAsync(); await Output.WriteStreamSuffixAsync();
if (_keepAlive) 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. // 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 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
ParsingRequestLine, ParsingRequestLine,
ParsingHeaders, ParsingHeaders,
AppStarted, AppStarted,
ResponseStarted HeadersCommitted,
HeadersFlushed
} }
} }

View File

@ -124,6 +124,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_collection[typeof(IHttpMinRequestBodyDataRateFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpMinRequestBodyDataRateFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpMinResponseDataRateFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpMinResponseDataRateFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpBodyControlFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpBodyControlFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpResponseStartFeature)] = CreateHttp1Connection();
CompareGenericGetterToIndexer(); CompareGenericGetterToIndexer();
@ -142,6 +143,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_collection.Set<IHttpMinRequestBodyDataRateFeature>(CreateHttp1Connection()); _collection.Set<IHttpMinRequestBodyDataRateFeature>(CreateHttp1Connection());
_collection.Set<IHttpMinResponseDataRateFeature>(CreateHttp1Connection()); _collection.Set<IHttpMinResponseDataRateFeature>(CreateHttp1Connection());
_collection.Set<IHttpBodyControlFeature>(CreateHttp1Connection()); _collection.Set<IHttpBodyControlFeature>(CreateHttp1Connection());
_collection.Set<IHttpResponseStartFeature>(CreateHttp1Connection());
CompareGenericGetterToIndexer(); CompareGenericGetterToIndexer();
@ -178,6 +180,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Same(_collection.Get<IHttpMinRequestBodyDataRateFeature>(), _collection[typeof(IHttpMinRequestBodyDataRateFeature)]); Assert.Same(_collection.Get<IHttpMinRequestBodyDataRateFeature>(), _collection[typeof(IHttpMinRequestBodyDataRateFeature)]);
Assert.Same(_collection.Get<IHttpMinResponseDataRateFeature>(), _collection[typeof(IHttpMinResponseDataRateFeature)]); Assert.Same(_collection.Get<IHttpMinResponseDataRateFeature>(), _collection[typeof(IHttpMinResponseDataRateFeature)]);
Assert.Same(_collection.Get<IHttpBodyControlFeature>(), _collection[typeof(IHttpBodyControlFeature)]); Assert.Same(_collection.Get<IHttpBodyControlFeature>(), _collection[typeof(IHttpBodyControlFeature)]);
Assert.Same(_collection.Get<IHttpResponseStartFeature>(), _collection[typeof(IHttpResponseStartFeature)]);
} }
private int EachHttpProtocolFeatureSetAndUnique() 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -1024,6 +1025,388 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); 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] [Fact]
public async Task ContentLength_Response_TooFewBytesWritten_Resets() public async Task ContentLength_Response_TooFewBytesWritten_Resets()
{ {

View File

@ -9,6 +9,7 @@ using System.Net;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
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; 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] [Fact]
public async Task ResponseBodyWriteAsyncCanBeCancelled() 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] [Fact]
public async Task OnStartingCallbacksAreCalledInLastInFirstOutOrder() public async Task OnStartingCallbacksAreCalledInLastInFirstOutOrder()
{ {

View File

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