diff --git a/src/Http/Http.Abstractions/src/HttpResponse.cs b/src/Http/Http.Abstractions/src/HttpResponse.cs
index e5af10fbfa..799b8b5cec 100644
--- a/src/Http/Http.Abstractions/src/HttpResponse.cs
+++ b/src/Http/Http.Abstractions/src/HttpResponse.cs
@@ -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.
/// True if the redirect is permanent (301), otherwise false (302).
public abstract void Redirect(string location, bool permanent);
+
+ ///
+ /// Starts the response by calling OnStarting() and making headers unmodifiable.
+ ///
+ ///
+ ///
+ /// If the isn't set, StartAsync will default to calling HttpResponse.Body.FlushAsync().
+ ///
+ public abstract Task StartAsync(CancellationToken cancellationToken = default);
}
}
diff --git a/src/Http/Http.Features/src/IHttpResponseStartFeature.cs b/src/Http/Http.Features/src/IHttpResponseStartFeature.cs
new file mode 100644
index 0000000000..45202a0871
--- /dev/null
+++ b/src/Http/Http.Features/src/IHttpResponseStartFeature.cs
@@ -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
+{
+ ///
+ /// Feature to start response writing.
+ ///
+ public interface IHttpResponseStartFeature
+ {
+ ///
+ /// Starts the response by calling OnStarting() and making headers unmodifiable.
+ ///
+ Task StartAsync(CancellationToken token = default);
+ }
+}
diff --git a/src/Http/Http/src/Internal/DefaultHttpResponse.cs b/src/Http/Http/src/Internal/DefaultHttpResponse.cs
index 0922da92f8..6fea6198bf 100644
--- a/src/Http/Http/src/Internal/DefaultHttpResponse.cs
+++ b/src/Http/Http/src/Internal/DefaultHttpResponse.cs
@@ -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 _nullResponseFeature = f => null;
+ private readonly static Func _nullResponseStartFeature = f => null;
private readonly static Func _newResponseCookiesFeature = f => new ResponseCookiesFeature(f);
private readonly static Func _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;
}
}
}
diff --git a/src/Http/Http/test/DefaultHttpContextTests.cs b/src/Http/Http/test/DefaultHttpContextTests.cs
index 5a54bc2726..2a7d7168a7 100644
--- a/src/Http/Http/test/DefaultHttpContextTests.cs
+++ b/src/Http/Http/test/DefaultHttpContextTests.cs
@@ -159,15 +159,16 @@ namespace Microsoft.AspNetCore.Http
features.Set(new HttpRequestFeature());
features.Set(new HttpResponseFeature());
features.Set(new TestHttpWebSocketFeature());
+ features.Set(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(new HttpRequestFeature());
newFeatures.Set(new HttpResponseFeature());
newFeatures.Set(new TestHttpWebSocketFeature());
+ newFeatures.Set(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();
+ }
+ }
}
}
diff --git a/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs
index 18c85701ec..3dfffb7ede 100644
--- a/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs
+++ b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs
@@ -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(() => context.Response.BodyPipe = null);
}
+ [Fact]
+ public async Task ResponseStart_CallsFeatureIfSet()
+ {
+ var features = new FeatureCollection();
+ var mock = new Mock();
+ mock.Setup(o => o.StartAsync(It.IsAny())).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);
+ }
+ }
}
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs
index cf40e3a9c0..7a25100788 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs
@@ -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);
+ }
}
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs
index 4b67300ba3..14abaab13c 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs
@@ -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(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature);
}
+ if (_currentIHttpResponseStartFeature != null)
+ {
+ yield return new KeyValuePair(IHttpResponseStartFeatureType, _currentIHttpResponseStartFeature);
+ }
if (_currentIHttpSendFileFeature != null)
{
yield return new KeyValuePair(IHttpSendFileFeatureType, _currentIHttpSendFileFeature);
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
index 8a8926b176..4f0f456da8 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
@@ -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 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 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 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)
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/RequestProcessingStatus.cs b/src/Servers/Kestrel/Core/src/Internal/Http/RequestProcessingStatus.cs
index f6e4248047..d32e4e3f9b 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/RequestProcessingStatus.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/RequestProcessingStatus.cs
@@ -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
}
-}
\ No newline at end of file
+}
diff --git a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs
index 011fa59fe1..24cce16454 100644
--- a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs
+++ b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs
@@ -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(CreateHttp1Connection());
_collection.Set(CreateHttp1Connection());
_collection.Set(CreateHttp1Connection());
+ _collection.Set(CreateHttp1Connection());
CompareGenericGetterToIndexer();
@@ -178,6 +180,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Same(_collection.Get(), _collection[typeof(IHttpMinRequestBodyDataRateFeature)]);
Assert.Same(_collection.Get(), _collection[typeof(IHttpMinResponseDataRateFeature)]);
Assert.Same(_collection.Get(), _collection[typeof(IHttpBodyControlFeature)]);
+ Assert.Same(_collection.Get(), _collection[typeof(IHttpResponseStartFeature)]);
}
private int EachHttpProtocolFeatureSetAndUnique()
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
index 46ce78701c..bc898995c0 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
@@ -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(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(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(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(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(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ };
+ await InitializeConnectionAsync(async context =>
+ {
+ await context.Response.StartAsync();
+ ex = Assert.Throws(() => 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(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(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