Skip to content

Commit f27a233

Browse files
committed
Implement Http/2 CompleteAsync #10886
1 parent a79bb2a commit f27a233

File tree

9 files changed

+829
-18
lines changed

9 files changed

+829
-18
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
6+
namespace Microsoft.AspNetCore.Http.Features
7+
{
8+
/// <summary>
9+
/// A feature to gracefully end a response.
10+
/// </summary>
11+
public interface IHttpResponseCompletionFeature
12+
{
13+
/// <summary>
14+
/// Flush any remaining response headers, data, or trailers.
15+
/// This may throw if the response is in an invalid state such as a Content-Length mismatch.
16+
/// </summary>
17+
/// <returns></returns>
18+
Task CompleteAsync();
19+
}
20+
}

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ protected void ResetHttp1Features()
277277
protected void ResetHttp2Features()
278278
{
279279
_currentIHttp2StreamIdFeature = this;
280+
_currentIHttpResponseCompletionFeature = this;
280281
_currentIHttpResponseTrailersFeature = this;
281282
}
282283

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ internal partial class HttpProtocol : IFeatureCollection
2929
private static readonly Type IFormFeatureType = typeof(IFormFeature);
3030
private static readonly Type IHttpUpgradeFeatureType = typeof(IHttpUpgradeFeature);
3131
private static readonly Type IHttp2StreamIdFeatureType = typeof(IHttp2StreamIdFeature);
32+
private static readonly Type IHttpResponseCompletionFeatureType = typeof(IHttpResponseCompletionFeature);
3233
private static readonly Type IHttpResponseTrailersFeatureType = typeof(IHttpResponseTrailersFeature);
3334
private static readonly Type IResponseCookiesFeatureType = typeof(IResponseCookiesFeature);
3435
private static readonly Type IItemsFeatureType = typeof(IItemsFeature);
@@ -58,6 +59,7 @@ internal partial class HttpProtocol : IFeatureCollection
5859
private object _currentIFormFeature;
5960
private object _currentIHttpUpgradeFeature;
6061
private object _currentIHttp2StreamIdFeature;
62+
private object _currentIHttpResponseCompletionFeature;
6163
private object _currentIHttpResponseTrailersFeature;
6264
private object _currentIResponseCookiesFeature;
6365
private object _currentIItemsFeature;
@@ -98,6 +100,7 @@ private void FastReset()
98100
_currentIQueryFeature = null;
99101
_currentIFormFeature = null;
100102
_currentIHttp2StreamIdFeature = null;
103+
_currentIHttpResponseCompletionFeature = null;
101104
_currentIHttpResponseTrailersFeature = null;
102105
_currentIResponseCookiesFeature = null;
103106
_currentIItemsFeature = null;
@@ -224,6 +227,10 @@ object IFeatureCollection.this[Type key]
224227
{
225228
feature = _currentIHttp2StreamIdFeature;
226229
}
230+
else if (key == IHttpResponseCompletionFeatureType)
231+
{
232+
feature = _currentIHttpResponseCompletionFeature;
233+
}
227234
else if (key == IHttpResponseTrailersFeatureType)
228235
{
229236
feature = _currentIHttpResponseTrailersFeature;
@@ -348,6 +355,10 @@ object IFeatureCollection.this[Type key]
348355
{
349356
_currentIHttp2StreamIdFeature = value;
350357
}
358+
else if (key == IHttpResponseCompletionFeatureType)
359+
{
360+
_currentIHttpResponseCompletionFeature = value;
361+
}
351362
else if (key == IHttpResponseTrailersFeatureType)
352363
{
353364
_currentIHttpResponseTrailersFeature = value;
@@ -470,6 +481,10 @@ TFeature IFeatureCollection.Get<TFeature>()
470481
{
471482
feature = (TFeature)_currentIHttp2StreamIdFeature;
472483
}
484+
else if (typeof(TFeature) == typeof(IHttpResponseCompletionFeature))
485+
{
486+
feature = (TFeature)_currentIHttpResponseCompletionFeature;
487+
}
473488
else if (typeof(TFeature) == typeof(IHttpResponseTrailersFeature))
474489
{
475490
feature = (TFeature)_currentIHttpResponseTrailersFeature;
@@ -598,6 +613,10 @@ void IFeatureCollection.Set<TFeature>(TFeature feature)
598613
{
599614
_currentIHttp2StreamIdFeature = feature;
600615
}
616+
else if (typeof(TFeature) == typeof(IHttpResponseCompletionFeature))
617+
{
618+
_currentIHttpResponseCompletionFeature = feature;
619+
}
601620
else if (typeof(TFeature) == typeof(IHttpResponseTrailersFeature))
602621
{
603622
_currentIHttpResponseTrailersFeature = feature;
@@ -718,6 +737,10 @@ private IEnumerable<KeyValuePair<Type, object>> FastEnumerable()
718737
{
719738
yield return new KeyValuePair<Type, object>(IHttp2StreamIdFeatureType, _currentIHttp2StreamIdFeature);
720739
}
740+
if (_currentIHttpResponseCompletionFeature != null)
741+
{
742+
yield return new KeyValuePair<Type, object>(IHttpResponseCompletionFeatureType, _currentIHttpResponseCompletionFeature);
743+
}
721744
if (_currentIHttpResponseTrailersFeature != null)
722745
{
723746
yield return new KeyValuePair<Type, object>(IHttpResponseTrailersFeatureType, _currentIHttpResponseTrailersFeature);

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ private void HttpVersionSetSlow(string value)
210210
public bool RequestTrailersAvailable { get; set; }
211211
public Stream RequestBody { get; set; }
212212
public PipeReader RequestBodyPipeReader { get; set; }
213+
public HttpResponseTrailers ResponseTrailers { get; set; }
213214

214215
private int _statusCode;
215216
public int StatusCode
@@ -287,7 +288,9 @@ public CancellationToken RequestAborted
287288

288289
public bool HasResponseStarted => _requestProcessingStatus >= RequestProcessingStatus.HeadersCommitted;
289290

290-
public bool HasFlushedHeaders => _requestProcessingStatus == RequestProcessingStatus.HeadersFlushed;
291+
public bool HasFlushedHeaders => _requestProcessingStatus >= RequestProcessingStatus.HeadersFlushed;
292+
293+
public bool HasResponseCompleted => _requestProcessingStatus == RequestProcessingStatus.ResponseCompleted;
291294

292295
protected HttpRequestHeaders HttpRequestHeaders { get; }
293296

@@ -632,9 +635,9 @@ private async Task ProcessRequests<TContext>(IHttpApplication<TContext> applicat
632635
// Run the application code for this request
633636
await application.ProcessRequestAsync(context);
634637

635-
if (!_connectionAborted)
638+
if (!_connectionAborted && !VerifyResponseContentLength(out var lengthException))
636639
{
637-
VerifyResponseContentLength();
640+
ReportApplicationError(lengthException);
638641
}
639642
}
640643
catch (BadHttpRequestException ex)
@@ -898,7 +901,7 @@ private void CheckLastWrite()
898901
}
899902
}
900903

901-
protected void VerifyResponseContentLength()
904+
protected bool VerifyResponseContentLength(out Exception ex)
902905
{
903906
var responseHeaders = HttpResponseHeaders;
904907

@@ -915,9 +918,13 @@ protected void VerifyResponseContentLength()
915918
_keepAlive = false;
916919
}
917920

918-
ReportApplicationError(new InvalidOperationException(
919-
CoreStrings.FormatTooFewBytesWritten(_responseBytesWritten, responseHeaders.ContentLength.Value)));
921+
ex = new InvalidOperationException(
922+
CoreStrings.FormatTooFewBytesWritten(_responseBytesWritten, responseHeaders.ContentLength.Value));
923+
return false;
920924
}
925+
926+
ex = null;
927+
return true;
921928
}
922929

923930
public void ProduceContinue()
@@ -935,7 +942,7 @@ public void ProduceContinue()
935942
}
936943
}
937944

938-
public Task InitializeResponseAsync(int firstWriteByteCount)
945+
public Task InitializeResponseAsync(int firstWriteByteCount, bool appCompleted = false)
939946
{
940947
var startingTask = FireOnStarting();
941948
// If return is Task.CompletedTask no awaiting is required
@@ -946,7 +953,7 @@ public Task InitializeResponseAsync(int firstWriteByteCount)
946953

947954
VerifyInitializeState(firstWriteByteCount);
948955

949-
ProduceStart(appCompleted: false);
956+
ProduceStart(appCompleted: appCompleted);
950957

951958
return Task.CompletedTask;
952959
}
@@ -1043,8 +1050,13 @@ protected Task ProduceEnd()
10431050
return WriteSuffix();
10441051
}
10451052

1046-
private Task WriteSuffix()
1053+
protected Task WriteSuffix()
10471054
{
1055+
if (HasResponseCompleted)
1056+
{
1057+
return Task.CompletedTask;
1058+
}
1059+
10481060
// _autoChunk should be checked after we are sure ProduceStart() has been called
10491061
// since ProduceStart() may set _autoChunk to true.
10501062
if (_autoChunk || _httpVersion == Http.HttpVersion.Http2)
@@ -1064,7 +1076,7 @@ private Task WriteSuffix()
10641076

10651077
if (!HasFlushedHeaders)
10661078
{
1067-
_requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;
1079+
_requestProcessingStatus = RequestProcessingStatus.ResponseCompleted;
10681080
return FlushAsyncInternal();
10691081
}
10701082

@@ -1080,6 +1092,8 @@ private async Task WriteSuffixAwaited()
10801092

10811093
await Output.WriteStreamSuffixAsync();
10821094

1095+
_requestProcessingStatus = RequestProcessingStatus.ResponseCompleted;
1096+
10831097
if (_keepAlive)
10841098
{
10851099
Log.ConnectionKeepAlive(ConnectionId);
@@ -1244,6 +1258,7 @@ private void SetErrorResponseHeaders(int statusCode)
12441258

12451259
var responseHeaders = HttpResponseHeaders;
12461260
responseHeaders.Reset();
1261+
ResponseTrailers?.Reset();
12471262
var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues();
12481263

12491264
responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);

src/Servers/Kestrel/Core/src/Internal/Http/RequestProcessingStatus.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal enum RequestProcessingStatus
1010
ParsingHeaders,
1111
AppStarted,
1212
HeadersCommitted,
13-
HeadersFlushed
13+
HeadersFlushed,
14+
ResponseCompleted
1415
}
1516
}

src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpRespon
151151
// 2. There is no trailing HEADERS frame.
152152
Http2HeadersFrameFlags http2HeadersFrame;
153153

154-
if (appCompleted && !_startedWritingDataFrames && (_stream.Trailers == null || _stream.Trailers.Count == 0))
154+
if (appCompleted && !_startedWritingDataFrames && (_stream.ResponseTrailers == null || _stream.ResponseTrailers.Count == 0))
155155
{
156156
_streamEnded = true;
157157
http2HeadersFrame = Http2HeadersFrameFlags.END_STREAM;
@@ -313,7 +313,7 @@ private async ValueTask<FlushResult> ProcessDataWrites()
313313
{
314314
readResult = await _dataPipe.Reader.ReadAsync();
315315

316-
if (readResult.IsCompleted && _stream.Trailers?.Count > 0)
316+
if (readResult.IsCompleted && _stream.ResponseTrailers?.Count > 0)
317317
{
318318
// Output is ending and there are trailers to write
319319
// Write any remaining content then write trailers
@@ -322,7 +322,8 @@ private async ValueTask<FlushResult> ProcessDataWrites()
322322
flushResult = await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: false);
323323
}
324324

325-
flushResult = await _frameWriter.WriteResponseTrailers(_streamId, _stream.Trailers);
325+
_stream.ResponseTrailers.SetReadOnly();
326+
flushResult = await _frameWriter.WriteResponseTrailers(_streamId, _stream.ResponseTrailers);
326327
}
327328
else if (readResult.IsCompleted && _streamEnded)
328329
{

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.FeatureCollection.cs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Threading.Tasks;
56
using Microsoft.AspNetCore.Http;
67
using Microsoft.AspNetCore.Http.Features;
78
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
@@ -11,21 +12,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
1112
{
1213
internal partial class Http2Stream : IHttp2StreamIdFeature,
1314
IHttpMinRequestBodyDataRateFeature,
15+
IHttpResponseCompletionFeature,
1416
IHttpResponseTrailersFeature
1517

1618
{
17-
internal HttpResponseTrailers Trailers { get; set; }
1819
private IHeaderDictionary _userTrailers;
1920

2021
IHeaderDictionary IHttpResponseTrailersFeature.Trailers
2122
{
2223
get
2324
{
24-
if (Trailers == null)
25+
if (ResponseTrailers == null)
2526
{
26-
Trailers = new HttpResponseTrailers();
27+
ResponseTrailers = new HttpResponseTrailers();
28+
if (HasResponseCompleted)
29+
{
30+
ResponseTrailers.SetReadOnly();
31+
}
2732
}
28-
return _userTrailers ?? Trailers;
33+
return _userTrailers ?? ResponseTrailers;
2934
}
3035
set
3136
{
@@ -48,5 +53,30 @@ MinDataRate IHttpMinRequestBodyDataRateFeature.MinDataRate
4853
MinRequestBodyDataRate = value;
4954
}
5055
}
56+
57+
async Task IHttpResponseCompletionFeature.CompleteAsync()
58+
{
59+
// Finalize headers
60+
if (!HasResponseStarted)
61+
{
62+
if (!VerifyResponseContentLength(out var lengthException))
63+
{
64+
throw lengthException;
65+
}
66+
67+
await InitializeResponseAsync(0, appCompleted: true);
68+
}
69+
70+
// Flush headers, body, trailers...
71+
if (!HasResponseCompleted)
72+
{
73+
if (!VerifyResponseContentLength(out var lengthException))
74+
{
75+
throw lengthException;
76+
}
77+
78+
await WriteSuffix();
79+
}
80+
}
5181
}
5282
}

0 commit comments

Comments
 (0)