Add StartAsync() to HttpResponse; ImplementFeature in Kestrel (#6967)
This commit is contained in:
parent
e1971f1d12
commit
1222d8de49
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 = $@"
|
||||
|
|
|
|||
Loading…
Reference in New Issue