| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162 |
- commit daf6e1ecd77410808cf01083b174ddbd5b9e88fa
- Author: Chris Ross (ASP.NET) <[email protected]>
- Date: Fri Sep 7 13:08:04 2018 -0700
- Http/2 response trailers #622
- diff --git a/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs b/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs
- index 0fc02aa7d9f..a98cfaf7f86 100644
- --- a/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs
- +++ b/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs
- @@ -49,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
- public void Http2ConnectionError(string connectionId, Http2ConnectionErrorException ex) { }
- public void Http2StreamError(string connectionId, Http2StreamErrorException ex) { }
- public void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex) { }
- + public void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex) { }
- public void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason) { }
- public void Http2ConnectionClosing(string connectionId) { }
- public void Http2ConnectionClosed(string connectionId, int highestOpenedStreamId) { }
- diff --git a/build/dependencies.props b/build/dependencies.props
- index 09cb28d3990..0abd7a09aa7 100644
- --- a/build/dependencies.props
- +++ b/build/dependencies.props
- @@ -11,13 +11,13 @@
- <MicrosoftAspNetCoreAllPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreAllPackageVersion>
- <MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
- <MicrosoftAspNetCoreCertificatesGenerationSourcesPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreCertificatesGenerationSourcesPackageVersion>
- - <MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
- - <MicrosoftAspNetCoreHostingPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreHostingPackageVersion>
- - <MicrosoftAspNetCoreHttpAbstractionsPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
- - <MicrosoftAspNetCoreHttpFeaturesPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreHttpFeaturesPackageVersion>
- - <MicrosoftAspNetCoreHttpPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreHttpPackageVersion>
- + <MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.2.0-a-preview3-tratcher-trailers-17154</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
- + <MicrosoftAspNetCoreHostingPackageVersion>2.2.0-a-preview3-tratcher-trailers-17154</MicrosoftAspNetCoreHostingPackageVersion>
- + <MicrosoftAspNetCoreHttpAbstractionsPackageVersion>2.2.0-a-preview3-tratcher-trailers-16760</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
- + <MicrosoftAspNetCoreHttpFeaturesPackageVersion>2.2.0-a-preview3-tratcher-trailers-16760</MicrosoftAspNetCoreHttpFeaturesPackageVersion>
- + <MicrosoftAspNetCoreHttpPackageVersion>2.2.0-a-preview3-tratcher-trailers-16760</MicrosoftAspNetCoreHttpPackageVersion>
- <MicrosoftAspNetCoreTestingPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreTestingPackageVersion>
- - <MicrosoftAspNetCoreWebUtilitiesPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreWebUtilitiesPackageVersion>
- + <MicrosoftAspNetCoreWebUtilitiesPackageVersion>2.2.0-a-preview3-tratcher-trailers-16760</MicrosoftAspNetCoreWebUtilitiesPackageVersion>
- <MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion>
- <MicrosoftExtensionsConfigurationBinderPackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsConfigurationBinderPackageVersion>
- <MicrosoftExtensionsConfigurationCommandLinePackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
- diff --git a/samples/Http2SampleApp/Startup.cs b/samples/Http2SampleApp/Startup.cs
- index 904e07cbb89..4f45eb97cc4 100644
- --- a/samples/Http2SampleApp/Startup.cs
- +++ b/samples/Http2SampleApp/Startup.cs
- @@ -14,6 +14,7 @@ namespace Http2SampleApp
-
- public void Configure(IApplicationBuilder app, IHostingEnvironment env)
- {
- + app.UseTimingMiddleware();
- app.Run(context =>
- {
- return context.Response.WriteAsync("Hello World! " + context.Request.Protocol);
- diff --git a/samples/Http2SampleApp/TimingMiddleware.cs b/samples/Http2SampleApp/TimingMiddleware.cs
- new file mode 100644
- index 00000000000..09bb1c80fff
- --- /dev/null
- +++ b/samples/Http2SampleApp/TimingMiddleware.cs
- @@ -0,0 +1,50 @@
- +using System.Diagnostics;
- +using System.Threading.Tasks;
- +using Microsoft.AspNetCore.Builder;
- +using Microsoft.AspNetCore.Http;
- +
- +namespace Http2SampleApp
- +{
- + // You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
- + public class TimingMiddleware
- + {
- + private readonly RequestDelegate _next;
- +
- + public TimingMiddleware(RequestDelegate next)
- + {
- + _next = next;
- + }
- +
- + public async Task Invoke(HttpContext httpContext)
- + {
- + if (httpContext.Response.SupportsTrailers())
- + {
- + httpContext.Response.DeclareTrailer("Server-Timing");
- +
- + var stopWatch = new Stopwatch();
- + stopWatch.Start();
- +
- + await _next(httpContext);
- +
- + stopWatch.Stop();
- + // Not yet supported in any browser dev tools
- + httpContext.Response.AppendTrailer("Server-Timing", $"app;dur={stopWatch.ElapsedMilliseconds}.0");
- + }
- + else
- + {
- + // Works in chrome
- + // httpContext.Response.Headers.Append("Server-Timing", $"app;dur=25.0");
- + await _next(httpContext);
- + }
- + }
- + }
- +
- + // Extension method used to add the middleware to the HTTP request pipeline.
- + public static class TimingMiddlewareExtensions
- + {
- + public static IApplicationBuilder UseTimingMiddleware(this IApplicationBuilder builder)
- + {
- + return builder.UseMiddleware<TimingMiddleware>();
- + }
- + }
- +}
- diff --git a/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs b/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs
- index daee31b4f05..7ac04398ed7 100644
- --- a/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs
- +++ b/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs
- @@ -8998,4 +8998,235 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- }
- }
- }
- +
- + public partial class HttpResponseTrailers
- + {
- + private static byte[] _headerBytes = new byte[]
- + {
- + 13,10,69,84,97,103,58,32,
- + };
- +
- + private long _bits = 0;
- + private HeaderReferences _headers;
- +
- +
- +
- + public StringValues HeaderETag
- + {
- + get
- + {
- + StringValues value;
- + if ((_bits & 1L) != 0)
- + {
- + value = _headers._ETag;
- + }
- + return value;
- + }
- + set
- + {
- + _bits |= 1L;
- + _headers._ETag = value;
- + }
- + }
- +
- + protected override int GetCountFast()
- + {
- + return (_contentLength.HasValue ? 1 : 0 ) + BitCount(_bits) + (MaybeUnknown?.Count ?? 0);
- + }
- +
- + protected override bool TryGetValueFast(string key, out StringValues value)
- + {
- + switch (key.Length)
- + {
- + case 4:
- + {
- + if ("ETag".Equals(key, StringComparison.OrdinalIgnoreCase))
- + {
- + if ((_bits & 1L) != 0)
- + {
- + value = _headers._ETag;
- + return true;
- + }
- + return false;
- + }
- + }
- + break;
- + }
- +
- + return MaybeUnknown?.TryGetValue(key, out value) ?? false;
- + }
- +
- + protected override void SetValueFast(string key, in StringValues value)
- + {
- + ValidateHeaderValueCharacters(value);
- + switch (key.Length)
- + {
- + case 4:
- + {
- + if ("ETag".Equals(key, StringComparison.OrdinalIgnoreCase))
- + {
- + _bits |= 1L;
- + _headers._ETag = value;
- + return;
- + }
- + }
- + break;
- + }
- +
- + SetValueUnknown(key, value);
- + }
- +
- + protected override bool AddValueFast(string key, in StringValues value)
- + {
- + ValidateHeaderValueCharacters(value);
- + switch (key.Length)
- + {
- + case 4:
- + {
- + if ("ETag".Equals(key, StringComparison.OrdinalIgnoreCase))
- + {
- + if ((_bits & 1L) == 0)
- + {
- + _bits |= 1L;
- + _headers._ETag = value;
- + return true;
- + }
- + return false;
- + }
- + }
- + break;
- + }
- +
- + Unknown.Add(key, value);
- + // Return true, above will throw and exit for false
- + return true;
- + }
- +
- + protected override bool RemoveFast(string key)
- + {
- + switch (key.Length)
- + {
- + case 4:
- + {
- + if ("ETag".Equals(key, StringComparison.OrdinalIgnoreCase))
- + {
- + if ((_bits & 1L) != 0)
- + {
- + _bits &= ~1L;
- + _headers._ETag = default(StringValues);
- + return true;
- + }
- + return false;
- + }
- + }
- + break;
- + }
- +
- + return MaybeUnknown?.Remove(key) ?? false;
- + }
- +
- + protected override void ClearFast()
- + {
- + MaybeUnknown?.Clear();
- + _contentLength = null;
- + var tempBits = _bits;
- + _bits = 0;
- + if(HttpHeaders.BitCount(tempBits) > 12)
- + {
- + _headers = default(HeaderReferences);
- + return;
- + }
- +
- + if ((tempBits & 1L) != 0)
- + {
- + _headers._ETag = default(StringValues);
- + if((tempBits & ~1L) == 0)
- + {
- + return;
- + }
- + tempBits &= ~1L;
- + }
- +
- + }
- +
- + protected override bool CopyToFast(KeyValuePair<string, StringValues>[] array, int arrayIndex)
- + {
- + if (arrayIndex < 0)
- + {
- + return false;
- + }
- +
- + if ((_bits & 1L) != 0)
- + {
- + if (arrayIndex == array.Length)
- + {
- + return false;
- + }
- + array[arrayIndex] = new KeyValuePair<string, StringValues>("ETag", _headers._ETag);
- + ++arrayIndex;
- + }
- + if (_contentLength.HasValue)
- + {
- + if (arrayIndex == array.Length)
- + {
- + return false;
- + }
- + array[arrayIndex] = new KeyValuePair<string, StringValues>("Content-Length", HeaderUtilities.FormatNonNegativeInt64(_contentLength.Value));
- + ++arrayIndex;
- + }
- + ((ICollection<KeyValuePair<string, StringValues>>)MaybeUnknown)?.CopyTo(array, arrayIndex);
- +
- + return true;
- + }
- +
- +
- +
- + private struct HeaderReferences
- + {
- + public StringValues _ETag;
- +
- + }
- +
- + public partial struct Enumerator
- + {
- + public bool MoveNext()
- + {
- + switch (_state)
- + {
- +
- + case 0:
- + goto state0;
- +
- + case 1:
- + goto state1;
- + default:
- + goto state_default;
- + }
- +
- + state0:
- + if ((_bits & 1L) != 0)
- + {
- + _current = new KeyValuePair<string, StringValues>("ETag", _collection._headers._ETag);
- + _state = 1;
- + return true;
- + }
- +
- + state1:
- + if (_collection._contentLength.HasValue)
- + {
- + _current = new KeyValuePair<string, StringValues>("Content-Length", HeaderUtilities.FormatNonNegativeInt64(_collection._contentLength.Value));
- + _state = 2;
- + return true;
- + }
- + state_default:
- + if (!_hasUnknown || !_unknownEnumerator.MoveNext())
- + {
- + _current = default(KeyValuePair<string, StringValues>);
- + return false;
- + }
- + _current = _unknownEnumerator.Current;
- + return true;
- + }
- + }
- + }
- }
- \ No newline at end of file
- diff --git a/src/Kestrel.Core/Internal/Http/HttpHeaders.cs b/src/Kestrel.Core/Internal/Http/HttpHeaders.cs
- index a0a7306d2f1..6acb332a26d 100644
- --- a/src/Kestrel.Core/Internal/Http/HttpHeaders.cs
- +++ b/src/Kestrel.Core/Internal/Http/HttpHeaders.cs
- @@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- throw new ArgumentException(CoreStrings.KeyAlreadyExists);
- }
-
- - int ICollection<KeyValuePair<string, StringValues>>.Count => GetCountFast();
- + public int Count => GetCountFast();
-
- bool ICollection<KeyValuePair<string, StringValues>>.IsReadOnly => _isReadOnly;
-
- diff --git a/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs b/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs
- index 7f3b047d70b..51b5daf836b 100644
- --- a/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs
- +++ b/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs
- @@ -209,9 +209,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- _currentIHttpUpgradeFeature = this;
- }
-
- - protected void ResetIHttp2StreamIdFeature()
- + protected void ResetHttp2Features()
- {
- _currentIHttp2StreamIdFeature = this;
- + _currentIHttpResponseTrailersFeature = this;
- }
-
- void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state)
- diff --git a/src/Kestrel.Core/Internal/Http/HttpProtocol.Generated.cs b/src/Kestrel.Core/Internal/Http/HttpProtocol.Generated.cs
- index 03ec02cc8ef..af2b5409623 100644
- --- a/src/Kestrel.Core/Internal/Http/HttpProtocol.Generated.cs
- +++ b/src/Kestrel.Core/Internal/Http/HttpProtocol.Generated.cs
- @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- private static readonly Type IFormFeatureType = typeof(IFormFeature);
- private static readonly Type IHttpUpgradeFeatureType = typeof(IHttpUpgradeFeature);
- private static readonly Type IHttp2StreamIdFeatureType = typeof(IHttp2StreamIdFeature);
- + private static readonly Type IHttpResponseTrailersFeatureType = typeof(IHttpResponseTrailersFeature);
- private static readonly Type IResponseCookiesFeatureType = typeof(IResponseCookiesFeature);
- private static readonly Type IItemsFeatureType = typeof(IItemsFeature);
- private static readonly Type ITlsConnectionFeatureType = typeof(ITlsConnectionFeature);
- @@ -46,6 +47,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- private object _currentIFormFeature;
- private object _currentIHttpUpgradeFeature;
- private object _currentIHttp2StreamIdFeature;
- + private object _currentIHttpResponseTrailersFeature;
- private object _currentIResponseCookiesFeature;
- private object _currentIItemsFeature;
- private object _currentITlsConnectionFeature;
- @@ -79,6 +81,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- _currentIFormFeature = null;
- _currentIHttpUpgradeFeature = null;
- _currentIHttp2StreamIdFeature = null;
- + _currentIHttpResponseTrailersFeature = null;
- _currentIResponseCookiesFeature = null;
- _currentIItemsFeature = null;
- _currentITlsConnectionFeature = null;
- @@ -183,6 +186,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- {
- feature = _currentIHttp2StreamIdFeature;
- }
- + else if (key == IHttpResponseTrailersFeatureType)
- + {
- + feature = _currentIHttpResponseTrailersFeature;
- + }
- else if (key == IResponseCookiesFeatureType)
- {
- feature = _currentIResponseCookiesFeature;
- @@ -279,6 +286,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- {
- _currentIHttp2StreamIdFeature = value;
- }
- + else if (key == IHttpResponseTrailersFeatureType)
- + {
- + _currentIHttpResponseTrailersFeature = value;
- + }
- else if (key == IResponseCookiesFeatureType)
- {
- _currentIResponseCookiesFeature = value;
- @@ -373,6 +384,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- {
- feature = (TFeature)_currentIHttp2StreamIdFeature;
- }
- + else if (typeof(TFeature) == typeof(IHttpResponseTrailersFeature))
- + {
- + feature = (TFeature)_currentIHttpResponseTrailersFeature;
- + }
- else if (typeof(TFeature) == typeof(IResponseCookiesFeature))
- {
- feature = (TFeature)_currentIResponseCookiesFeature;
- @@ -473,6 +488,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- {
- _currentIHttp2StreamIdFeature = feature;
- }
- + else if (typeof(TFeature) == typeof(IHttpResponseTrailersFeature))
- + {
- + _currentIHttpResponseTrailersFeature = feature;
- + }
- else if (typeof(TFeature) == typeof(IResponseCookiesFeature))
- {
- _currentIResponseCookiesFeature = feature;
- @@ -565,6 +584,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- {
- yield return new KeyValuePair<Type, object>(IHttp2StreamIdFeatureType, _currentIHttp2StreamIdFeature);
- }
- + if (_currentIHttpResponseTrailersFeature != null)
- + {
- + yield return new KeyValuePair<Type, object>(IHttpResponseTrailersFeatureType, _currentIHttpResponseTrailersFeature);
- + }
- if (_currentIResponseCookiesFeature != null)
- {
- yield return new KeyValuePair<Type, object>(IResponseCookiesFeatureType, _currentIResponseCookiesFeature);
- diff --git a/src/Kestrel.Core/Internal/Http/HttpResponseTrailers.cs b/src/Kestrel.Core/Internal/Http/HttpResponseTrailers.cs
- new file mode 100644
- index 00000000000..4e910e31a66
- --- /dev/null
- +++ b/src/Kestrel.Core/Internal/Http/HttpResponseTrailers.cs
- @@ -0,0 +1,65 @@
- +// 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.Collections;
- +using System.Collections.Generic;
- +using System.Runtime.CompilerServices;
- +using Microsoft.Extensions.Primitives;
- +
- +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- +{
- + public partial class HttpResponseTrailers : HttpHeaders
- + {
- + public Enumerator GetEnumerator()
- + {
- + return new Enumerator(this);
- + }
- +
- + protected override IEnumerator<KeyValuePair<string, StringValues>> GetEnumeratorFast()
- + {
- + return GetEnumerator();
- + }
- +
- + [MethodImpl(MethodImplOptions.NoInlining)]
- + private void SetValueUnknown(string key, in StringValues value)
- + {
- + ValidateHeaderNameCharacters(key);
- + Unknown[key] = value;
- + }
- +
- + public partial struct Enumerator : IEnumerator<KeyValuePair<string, StringValues>>
- + {
- + private readonly HttpResponseTrailers _collection;
- + private readonly long _bits;
- + private int _state;
- + private KeyValuePair<string, StringValues> _current;
- + private readonly bool _hasUnknown;
- + private Dictionary<string, StringValues>.Enumerator _unknownEnumerator;
- +
- + internal Enumerator(HttpResponseTrailers collection)
- + {
- + _collection = collection;
- + _bits = collection._bits;
- + _state = 0;
- + _current = default;
- + _hasUnknown = collection.MaybeUnknown != null;
- + _unknownEnumerator = _hasUnknown
- + ? collection.MaybeUnknown.GetEnumerator()
- + : default;
- + }
- +
- + public KeyValuePair<string, StringValues> Current => _current;
- +
- + object IEnumerator.Current => _current;
- +
- + public void Dispose()
- + {
- + }
- +
- + public void Reset()
- + {
- + _state = 0;
- + }
- + }
- + }
- +}
- diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
- index 87c6c4ed3b6..e3968fc14df 100644
- --- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
- +++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
- @@ -90,7 +90,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
- var http2Limits = httpLimits.Http2;
-
- _context = context;
- - _frameWriter = new Http2FrameWriter(context.Transport.Output, context.ConnectionContext, _outputFlowControl, context.TimeoutControl, context.ConnectionId, context.ServiceContext.Log);
- + _frameWriter = new Http2FrameWriter(context.Transport.Output, context.ConnectionContext, this, _outputFlowControl, context.TimeoutControl, context.ConnectionId, context.ServiceContext.Log);
- _serverSettings.MaxConcurrentStreams = (uint)http2Limits.MaxStreamsPerConnection;
- _serverSettings.MaxFrameSize = (uint)http2Limits.MaxFrameSize;
- _serverSettings.HeaderTableSize = (uint)http2Limits.HeaderTableSize;
- diff --git a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs
- index f66f1627072..cd8bc7ae482 100644
- --- a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs
- +++ b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs
- @@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
- private readonly PipeWriter _outputWriter;
- private bool _aborted;
- private readonly ConnectionContext _connectionContext;
- + private readonly Http2Connection _http2Connection;
- private readonly OutputFlowControl _connectionOutputFlowControl;
- private readonly string _connectionId;
- private readonly IKestrelTrace _log;
- @@ -41,6 +42,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
- public Http2FrameWriter(
- PipeWriter outputPipeWriter,
- ConnectionContext connectionContext,
- + Http2Connection http2Connection,
- OutputFlowControl connectionOutputFlowControl,
- ITimeoutControl timeoutControl,
- string connectionId,
- @@ -48,6 +50,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
- {
- _outputWriter = outputPipeWriter;
- _connectionContext = connectionContext;
- + _http2Connection = http2Connection;
- _connectionOutputFlowControl = connectionOutputFlowControl;
- _connectionId = connectionId;
- _log = log;
- @@ -157,39 +160,69 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
- _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId);
- var buffer = _headerEncodingBuffer.AsSpan();
- var done = _hpackEncoder.BeginEncode(statusCode, EnumerateHeaders(headers), buffer, out var payloadLength);
- + FinishWritingHeaders(streamId, payloadLength, done);
- + }
- + catch (HPackEncodingException hex)
- + {
- + _log.HPackEncodingError(_connectionId, streamId, hex);
- + _http2Connection.Abort(new ConnectionAbortedException(hex.Message, hex));
- + throw new InvalidOperationException(hex.Message, hex); // Report the error to the user if this was the first write.
- + }
- + }
- + }
-
- - _outgoingFrame.PayloadLength = payloadLength;
- + public Task WriteResponseTrailers(int streamId, HttpResponseTrailers headers)
- + {
- + lock (_writeLock)
- + {
- + if (_completed)
- + {
- + return Task.CompletedTask;
- + }
-
- - if (done)
- - {
- - _outgoingFrame.HeadersFlags = Http2HeadersFrameFlags.END_HEADERS;
- - }
- + try
- + {
- + _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_STREAM, streamId);
- + var buffer = _headerEncodingBuffer.AsSpan();
- + var done = _hpackEncoder.BeginEncode(EnumerateHeaders(headers), buffer, out var payloadLength);
- + FinishWritingHeaders(streamId, payloadLength, done);
- + }
- + catch (HPackEncodingException hex)
- + {
- + _log.HPackEncodingError(_connectionId, streamId, hex);
- + _http2Connection.Abort(new ConnectionAbortedException(hex.Message, hex));
- + }
-
- - WriteHeaderUnsynchronized();
- - _outputWriter.Write(buffer.Slice(0, payloadLength));
- + return _flusher.FlushAsync();
- + }
- + }
-
- - while (!done)
- - {
- - _outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId);
- + private void FinishWritingHeaders(int streamId, int payloadLength, bool done)
- + {
- + var buffer = _headerEncodingBuffer.AsSpan();
- + _outgoingFrame.PayloadLength = payloadLength;
- + if (done)
- + {
- + _outgoingFrame.HeadersFlags |= Http2HeadersFrameFlags.END_HEADERS;
- + }
-
- - done = _hpackEncoder.Encode(buffer, out payloadLength);
- - _outgoingFrame.PayloadLength = payloadLength;
- + WriteHeaderUnsynchronized();
- + _outputWriter.Write(buffer.Slice(0, payloadLength));
-
- - if (done)
- - {
- - _outgoingFrame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS;
- - }
- + while (!done)
- + {
- + _outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId);
-
- - WriteHeaderUnsynchronized();
- - _outputWriter.Write(buffer.Slice(0, payloadLength));
- - }
- - }
- - catch (HPackEncodingException hex)
- + done = _hpackEncoder.Encode(buffer, out payloadLength);
- + _outgoingFrame.PayloadLength = payloadLength;
- +
- + if (done)
- {
- - // Header errors are fatal to the connection. We don't have a direct way to signal this to the Http2Connection.
- - _connectionContext.Abort(new ConnectionAbortedException("", hex));
- - throw new InvalidOperationException("", hex); // Report the error to the user if this was the first write.
- + _outgoingFrame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS;
- }
- +
- + WriteHeaderUnsynchronized();
- + _outputWriter.Write(buffer.Slice(0, payloadLength));
- }
- }
-
- diff --git a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs
- index 812a2d9fca9..1485b65681a 100644
- --- a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs
- +++ b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs
- @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
- // This should only be accessed via the FrameWriter. The connection-level output flow control is protected by the
- // FrameWriter's connection-level write lock.
- private readonly StreamOutputFlowControl _flowControl;
- -
- + private readonly Http2Stream _stream;
- private readonly object _dataWriterLock = new object();
- private readonly Pipe _dataPipe;
- private readonly Task _dataWriteProcessingTask;
- @@ -37,11 +37,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
- Http2FrameWriter frameWriter,
- StreamOutputFlowControl flowControl,
- ITimeoutControl timeoutControl,
- - MemoryPool<byte> pool)
- + MemoryPool<byte> pool,
- + Http2Stream stream)
- {
- _streamId = streamId;
- _frameWriter = frameWriter;
- _flowControl = flowControl;
- + _stream = stream;
- _dataPipe = CreateDataPipe(pool);
- _flusher = new TimingPipeFlusher(_dataPipe.Writer, timeoutControl);
- _dataWriteProcessingTask = ProcessDataWrites();
- @@ -200,7 +202,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
- {
- readResult = await _dataPipe.Reader.ReadAsync();
-
- - await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: readResult.IsCompleted);
- + if (readResult.IsCompleted && _stream.Trailers?.Count > 0)
- + {
- + if (readResult.Buffer.Length > 0)
- + {
- + await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: false);
- + }
- +
- + await _frameWriter.WriteResponseTrailers(_streamId, _stream.Trailers);
- + }
- + else
- + {
- + await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: readResult.IsCompleted);
- + }
-
- _dataPipe.Reader.AdvanceTo(readResult.Buffer.End);
- } while (!readResult.IsCompleted);
- diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.FeatureCollection.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.FeatureCollection.cs
- index 782a8ddf51d..72604950585 100644
- --- a/src/Kestrel.Core/Internal/Http2/Http2Stream.FeatureCollection.cs
- +++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.FeatureCollection.cs
- @@ -1,12 +1,34 @@
- // 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 Microsoft.AspNetCore.Http;
- +using Microsoft.AspNetCore.Http.Features;
- using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
- +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
-
- namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
- {
- - public partial class Http2Stream : IHttp2StreamIdFeature
- + public partial class Http2Stream : IHttp2StreamIdFeature, IHttpResponseTrailersFeature
- {
- + internal HttpResponseTrailers Trailers { get; set; }
- + private IHeaderDictionary _userTrailers;
- +
- + IHeaderDictionary IHttpResponseTrailersFeature.Trailers
- + {
- + get
- + {
- + if (Trailers == null)
- + {
- + Trailers = new HttpResponseTrailers();
- + }
- + return _userTrailers ?? Trailers;
- + }
- + set
- + {
- + _userTrailers = value;
- + }
- + }
- +
- int IHttp2StreamIdFeature.StreamId => _context.StreamId;
- }
- }
- diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
- index be7c9b9d091..79d93b2a4c9 100644
- --- a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
- +++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
- @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
- _context.ServerPeerSettings.InitialWindowSize / 2);
-
- _outputFlowControl = new StreamOutputFlowControl(context.ConnectionOutputFlowControl, context.ClientPeerSettings.InitialWindowSize);
- - _http2Output = new Http2OutputProducer(context.StreamId, context.FrameWriter, _outputFlowControl, context.TimeoutControl, context.MemoryPool);
- + _http2Output = new Http2OutputProducer(context.StreamId, context.FrameWriter, _outputFlowControl, context.TimeoutControl, context.MemoryPool, this);
-
- RequestBodyPipe = CreateRequestBodyPipe(_context.ServerPeerSettings.InitialWindowSize);
- Output = _http2Output;
- @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
-
- protected override void OnReset()
- {
- - ResetIHttp2StreamIdFeature();
- + ResetHttp2Features();
- }
-
- protected override void OnRequestProcessingEnded()
- diff --git a/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs b/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
- index 19ecc86ee2b..852fea7953d 100644
- --- a/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
- +++ b/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
- @@ -67,6 +67,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
-
- void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex);
-
- + void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex);
- +
- void Http2FrameReceived(string connectionId, Http2Frame frame);
-
- void Http2FrameSending(string connectionId, Http2Frame frame);
- diff --git a/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs b/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
- index f735370d3e4..39bf9d2895e 100644
- --- a/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
- +++ b/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
- @@ -107,6 +107,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
- LoggerMessage.Define<string, Http2FrameType, int, int, object>(LogLevel.Trace, new EventId(37, nameof(Http2FrameReceived)),
- @"Connection id ""{ConnectionId}"" sending {type} frame for stream ID {id} with length {length} and flags {flags}");
-
- + private static readonly Action<ILogger, string, int, Exception> _hpackEncodingError =
- + LoggerMessage.Define<string, int>(LogLevel.Information, new EventId(38, nameof(HPackEncodingError)),
- + @"Connection id ""{ConnectionId}"": HPACK encoding error while encoding headers for stream ID {StreamId}.");
- +
- protected readonly ILogger _logger;
-
- public KestrelTrace(ILogger logger)
- @@ -254,6 +258,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
- _hpackDecodingError(_logger, connectionId, streamId, ex);
- }
-
- + public virtual void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex)
- + {
- + _hpackEncodingError(_logger, connectionId, streamId, ex);
- + }
- +
- public void Http2FrameReceived(string connectionId, Http2Frame frame)
- {
- _http2FrameReceived(_logger, connectionId, frame.Type, frame.StreamId, frame.PayloadLength, frame.ShowFlags(), null);
- diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs
- index 50d52558543..74912e65b6a 100644
- --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs
- +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs
- @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Http;
- using Microsoft.AspNetCore.Http.Features;
- using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
- using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
- +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
- using Microsoft.Extensions.Logging;
- using Microsoft.Net.Http.Headers;
- using Xunit;
- @@ -1340,6 +1341,223 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
- Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
- }
-
- + [Fact]
- + public async Task ResponseTrailers_WithoutData_Sent()
- + {
- + await InitializeConnectionAsync(context =>
- + {
- + context.Response.AppendTrailer("CustomName", "Custom Value");
- + return Task.CompletedTask;
- + });
- +
- + await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
- +
- + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- + withLength: 55,
- + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- + withStreamId: 1);
- +
- + var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- + withLength: 25,
- + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
- + withStreamId: 1);
- +
- + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
- +
- + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, 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]);
- +
- + _decodedHeaders.Clear();
- +
- + _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
- +
- + Assert.Single(_decodedHeaders);
- + Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
- + }
- +
- + [Fact]
- + public async Task ResponseTrailers_WithData_Sent()
- + {
- + await InitializeConnectionAsync(async context =>
- + {
- + await context.Response.WriteAsync("Hello World");
- + context.Response.AppendTrailer("CustomName", "Custom Value");
- + });
- +
- + await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
- +
- + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- + withLength: 37,
- + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- + withStreamId: 1);
- +
- + await ExpectAsync(Http2FrameType.DATA,
- + withLength: 11,
- + withFlags: (byte)Http2DataFrameFlags.NONE,
- + withStreamId: 1);
- +
- + var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- + withLength: 25,
- + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
- + withStreamId: 1);
- +
- + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
- +
- + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
- +
- + Assert.Equal(2, _decodedHeaders.Count);
- + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
- +
- + _decodedHeaders.Clear();
- +
- + _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
- +
- + Assert.Single(_decodedHeaders);
- + Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
- + }
- +
- + [Fact]
- + public async Task ResponseTrailers_WithContinuation_Sent()
- + {
- + var largeHeader = new string('a', 1024 * 3);
- + await InitializeConnectionAsync(async context =>
- + {
- + await context.Response.WriteAsync("Hello World");
- + // The first five fill the first frame
- + context.Response.AppendTrailer("CustomName0", largeHeader);
- + context.Response.AppendTrailer("CustomName1", largeHeader);
- + context.Response.AppendTrailer("CustomName2", largeHeader);
- + context.Response.AppendTrailer("CustomName3", largeHeader);
- + context.Response.AppendTrailer("CustomName4", largeHeader);
- + // This one spills over to the next frame
- + context.Response.AppendTrailer("CustomName5", largeHeader);
- + });
- +
- + await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
- +
- + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- + withLength: 37,
- + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- + withStreamId: 1);
- +
- + await ExpectAsync(Http2FrameType.DATA,
- + withLength: 11,
- + withFlags: (byte)Http2DataFrameFlags.NONE,
- + withStreamId: 1);
- +
- + var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- + withLength: 15440,
- + withFlags: (byte)Http2HeadersFrameFlags.END_STREAM,
- + withStreamId: 1);
- +
- + var trailersContinuationFrame = await ExpectAsync(Http2FrameType.CONTINUATION,
- + withLength: 3088,
- + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- + withStreamId: 1);
- +
- + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
- +
- + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
- +
- + Assert.Equal(2, _decodedHeaders.Count);
- + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
- +
- + _decodedHeaders.Clear();
- +
- + _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: false, handler: this);
- +
- + Assert.Equal(5, _decodedHeaders.Count);
- + Assert.Equal(largeHeader, _decodedHeaders["CustomName0"]);
- + Assert.Equal(largeHeader, _decodedHeaders["CustomName1"]);
- + Assert.Equal(largeHeader, _decodedHeaders["CustomName2"]);
- + Assert.Equal(largeHeader, _decodedHeaders["CustomName3"]);
- + Assert.Equal(largeHeader, _decodedHeaders["CustomName4"]);
- +
- + _decodedHeaders.Clear();
- +
- + _hpackDecoder.Decode(trailersContinuationFrame.PayloadSequence, endHeaders: true, handler: this);
- +
- + Assert.Single(_decodedHeaders);
- + Assert.Equal(largeHeader, _decodedHeaders["CustomName5"]);
- + }
- +
- + [Fact]
- + public async Task ResponseTrailers_WithNonAscii_Throws()
- + {
- + await InitializeConnectionAsync(async context =>
- + {
- + await context.Response.WriteAsync("Hello World");
- + Assert.Throws<InvalidOperationException>(() => context.Response.AppendTrailer("Custom你好Name", "Custom Value"));
- + Assert.Throws<InvalidOperationException>(() => context.Response.AppendTrailer("CustomName", "Custom 你好 Value"));
- + });
- +
- + await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
- +
- + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- + withLength: 37,
- + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- + withStreamId: 1);
- +
- + await ExpectAsync(Http2FrameType.DATA,
- + withLength: 11,
- + 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: true, handler: this);
- +
- + Assert.Equal(2, _decodedHeaders.Count);
- + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
- + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
- + }
- +
- + [Fact]
- + public async Task ResponseTrailers_TooLong_Throws()
- + {
- + await InitializeConnectionAsync(async context =>
- + {
- + await context.Response.WriteAsync("Hello World");
- + context.Response.AppendTrailer("too_long", new string('a', (int)Http2PeerSettings.DefaultMaxFrameSize));
- + });
- +
- + await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
- +
- + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- + withLength: 37,
- + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- + withStreamId: 1);
- +
- + await ExpectAsync(Http2FrameType.DATA,
- + withLength: 11,
- + withFlags: (byte)Http2DataFrameFlags.NONE,
- + withStreamId: 1);
- +
- + var goAway = await ExpectAsync(Http2FrameType.GOAWAY,
- + withLength: 8,
- + withFlags: (byte)Http2DataFrameFlags.NONE,
- + withStreamId: 0);
- +
- + VerifyGoAway(goAway, 1, Http2ErrorCode.INTERNAL_ERROR);
- +
- + _pair.Application.Output.Complete();
- + await _connectionTask;
- +
- + var message = Assert.Single(TestApplicationErrorLogger.Messages, m => m.Exception is HPackEncodingException);
- + Assert.Contains(CoreStrings.HPackErrorNotEnoughBuffer, message.Exception.Message);
- + }
- +
- [Fact]
- public async Task ApplicationException_BeforeFirstWrite_Sends500()
- {
- @@ -1750,6 +1968,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
-
- var message = await appFinished.Task.DefaultTimeout();
- Assert.Equal(CoreStrings.HPackErrorNotEnoughBuffer, message);
- +
- + // Just the StatusCode gets written before aborting in the continuation frame
- + await ExpectAsync(Http2FrameType.HEADERS,
- + withLength: 37,
- + withFlags: (byte)Http2HeadersFrameFlags.NONE,
- + withStreamId: 1);
- +
- + _pair.Application.Output.Complete();
- +
- + await WaitForConnectionErrorAsync<HPackEncodingException>(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR,
- + CoreStrings.HPackErrorNotEnoughBuffer);
- }
- }
- }
- \ No newline at end of file
- diff --git a/test/shared/CompositeKestrelTrace.cs b/test/shared/CompositeKestrelTrace.cs
- index 8c9be6c1bc2..486db3987a2 100644
- --- a/test/shared/CompositeKestrelTrace.cs
- +++ b/test/shared/CompositeKestrelTrace.cs
- @@ -189,6 +189,12 @@ namespace Microsoft.AspNetCore.Testing
- _trace2.HPackDecodingError(connectionId, streamId, ex);
- }
-
- + public void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex)
- + {
- + _trace1.HPackEncodingError(connectionId, streamId, ex);
- + _trace2.HPackEncodingError(connectionId, streamId, ex);
- + }
- +
- public void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason)
- {
- _trace1.Http2StreamResetAbort(traceIdentifier, error, abortReason);
- diff --git a/tools/CodeGenerator/HttpProtocolFeatureCollection.cs b/tools/CodeGenerator/HttpProtocolFeatureCollection.cs
- index 61025768323..85e41da0fbd 100644
- --- a/tools/CodeGenerator/HttpProtocolFeatureCollection.cs
- +++ b/tools/CodeGenerator/HttpProtocolFeatureCollection.cs
- @@ -30,6 +30,7 @@ namespace CodeGenerator
- {
- "IHttpUpgradeFeature",
- "IHttp2StreamIdFeature",
- + "IHttpResponseTrailersFeature",
- "IResponseCookiesFeature",
- "IItemsFeature",
- "ITlsConnectionFeature",
- diff --git a/tools/CodeGenerator/KnownHeaders.cs b/tools/CodeGenerator/KnownHeaders.cs
- index 5e16debf1df..2461796c17e 100644
- --- a/tools/CodeGenerator/KnownHeaders.cs
- +++ b/tools/CodeGenerator/KnownHeaders.cs
- @@ -269,6 +269,21 @@ namespace CodeGenerator
- PrimaryHeader = responsePrimaryHeaders.Contains("Content-Length")
- }})
- .ToArray();
- +
- + var responseTrailers = new[]
- + {
- + "ETag",
- + }
- + .Select((header, index) => new KnownHeader
- + {
- + Name = header,
- + Index = index,
- + EnhancedSetter = enhancedHeaders.Contains(header),
- + ExistenceCheck = responseHeadersExistence.Contains(header),
- + PrimaryHeader = responsePrimaryHeaders.Contains(header)
- + })
- + .ToArray();
- +
- // 63 for responseHeaders as it steals one bit for Content-Length in CopyTo(ref MemoryPoolIterator output)
- Debug.Assert(responseHeaders.Length <= 63);
- Debug.Assert(responseHeaders.Max(x => x.Index) <= 62);
- @@ -288,6 +303,13 @@ namespace CodeGenerator
- HeadersByLength = responseHeaders.GroupBy(x => x.Name.Length),
- ClassName = "HttpResponseHeaders",
- Bytes = responseHeaders.SelectMany(header => header.Bytes).ToArray()
- + },
- + new
- + {
- + Headers = responseTrailers,
- + HeadersByLength = responseTrailers.GroupBy(x => x.Name.Length),
- + ClassName = "HttpResponseTrailers",
- + Bytes = responseTrailers.SelectMany(header => header.Bytes).ToArray()
- }
- };
- foreach (var loop in loops.Where(l => l.Bytes != null))
- @@ -402,7 +424,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- }}
-
- protected override void SetValueFast(string key, in StringValues value)
- - {{{(loop.ClassName == "HttpResponseHeaders" ? @"
- + {{{(loop.ClassName != "HttpRequestHeaders" ? @"
- ValidateHeaderValueCharacters(value);" : "")}
- switch (key.Length)
- {{{Each(loop.HeadersByLength, byLength => $@"
- @@ -424,7 +446,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
- }}
-
- protected override bool AddValueFast(string key, in StringValues value)
- - {{{(loop.ClassName == "HttpResponseHeaders" ? @"
- + {{{(loop.ClassName != "HttpRequestHeaders" ? @"
- ValidateHeaderValueCharacters(value);" : "")}
- switch (key.Length)
- {{{Each(loop.HeadersByLength, byLength => $@"
|