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;
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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 = $@"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue