2
0

KestrelHttpServer 266 B

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162
  1. commit daf6e1ecd77410808cf01083b174ddbd5b9e88fa
  2. Author: Chris Ross (ASP.NET) <[email protected]>
  3. Date: Fri Sep 7 13:08:04 2018 -0700
  4. Http/2 response trailers #622
  5. diff --git a/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs b/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs
  6. index 0fc02aa7d9f..a98cfaf7f86 100644
  7. --- a/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs
  8. +++ b/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs
  9. @@ -49,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
  10. public void Http2ConnectionError(string connectionId, Http2ConnectionErrorException ex) { }
  11. public void Http2StreamError(string connectionId, Http2StreamErrorException ex) { }
  12. public void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex) { }
  13. + public void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex) { }
  14. public void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason) { }
  15. public void Http2ConnectionClosing(string connectionId) { }
  16. public void Http2ConnectionClosed(string connectionId, int highestOpenedStreamId) { }
  17. diff --git a/build/dependencies.props b/build/dependencies.props
  18. index 09cb28d3990..0abd7a09aa7 100644
  19. --- a/build/dependencies.props
  20. +++ b/build/dependencies.props
  21. @@ -11,13 +11,13 @@
  22. <MicrosoftAspNetCoreAllPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreAllPackageVersion>
  23. <MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
  24. <MicrosoftAspNetCoreCertificatesGenerationSourcesPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreCertificatesGenerationSourcesPackageVersion>
  25. - <MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
  26. - <MicrosoftAspNetCoreHostingPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreHostingPackageVersion>
  27. - <MicrosoftAspNetCoreHttpAbstractionsPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
  28. - <MicrosoftAspNetCoreHttpFeaturesPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreHttpFeaturesPackageVersion>
  29. - <MicrosoftAspNetCoreHttpPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreHttpPackageVersion>
  30. + <MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.2.0-a-preview3-tratcher-trailers-17154</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
  31. + <MicrosoftAspNetCoreHostingPackageVersion>2.2.0-a-preview3-tratcher-trailers-17154</MicrosoftAspNetCoreHostingPackageVersion>
  32. + <MicrosoftAspNetCoreHttpAbstractionsPackageVersion>2.2.0-a-preview3-tratcher-trailers-16760</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
  33. + <MicrosoftAspNetCoreHttpFeaturesPackageVersion>2.2.0-a-preview3-tratcher-trailers-16760</MicrosoftAspNetCoreHttpFeaturesPackageVersion>
  34. + <MicrosoftAspNetCoreHttpPackageVersion>2.2.0-a-preview3-tratcher-trailers-16760</MicrosoftAspNetCoreHttpPackageVersion>
  35. <MicrosoftAspNetCoreTestingPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreTestingPackageVersion>
  36. - <MicrosoftAspNetCoreWebUtilitiesPackageVersion>2.2.0-preview3-35359</MicrosoftAspNetCoreWebUtilitiesPackageVersion>
  37. + <MicrosoftAspNetCoreWebUtilitiesPackageVersion>2.2.0-a-preview3-tratcher-trailers-16760</MicrosoftAspNetCoreWebUtilitiesPackageVersion>
  38. <MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion>
  39. <MicrosoftExtensionsConfigurationBinderPackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsConfigurationBinderPackageVersion>
  40. <MicrosoftExtensionsConfigurationCommandLinePackageVersion>2.2.0-preview3-35359</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
  41. diff --git a/samples/Http2SampleApp/Startup.cs b/samples/Http2SampleApp/Startup.cs
  42. index 904e07cbb89..4f45eb97cc4 100644
  43. --- a/samples/Http2SampleApp/Startup.cs
  44. +++ b/samples/Http2SampleApp/Startup.cs
  45. @@ -14,6 +14,7 @@ namespace Http2SampleApp
  46. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  47. {
  48. + app.UseTimingMiddleware();
  49. app.Run(context =>
  50. {
  51. return context.Response.WriteAsync("Hello World! " + context.Request.Protocol);
  52. diff --git a/samples/Http2SampleApp/TimingMiddleware.cs b/samples/Http2SampleApp/TimingMiddleware.cs
  53. new file mode 100644
  54. index 00000000000..09bb1c80fff
  55. --- /dev/null
  56. +++ b/samples/Http2SampleApp/TimingMiddleware.cs
  57. @@ -0,0 +1,50 @@
  58. +using System.Diagnostics;
  59. +using System.Threading.Tasks;
  60. +using Microsoft.AspNetCore.Builder;
  61. +using Microsoft.AspNetCore.Http;
  62. +
  63. +namespace Http2SampleApp
  64. +{
  65. + // You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
  66. + public class TimingMiddleware
  67. + {
  68. + private readonly RequestDelegate _next;
  69. +
  70. + public TimingMiddleware(RequestDelegate next)
  71. + {
  72. + _next = next;
  73. + }
  74. +
  75. + public async Task Invoke(HttpContext httpContext)
  76. + {
  77. + if (httpContext.Response.SupportsTrailers())
  78. + {
  79. + httpContext.Response.DeclareTrailer("Server-Timing");
  80. +
  81. + var stopWatch = new Stopwatch();
  82. + stopWatch.Start();
  83. +
  84. + await _next(httpContext);
  85. +
  86. + stopWatch.Stop();
  87. + // Not yet supported in any browser dev tools
  88. + httpContext.Response.AppendTrailer("Server-Timing", $"app;dur={stopWatch.ElapsedMilliseconds}.0");
  89. + }
  90. + else
  91. + {
  92. + // Works in chrome
  93. + // httpContext.Response.Headers.Append("Server-Timing", $"app;dur=25.0");
  94. + await _next(httpContext);
  95. + }
  96. + }
  97. + }
  98. +
  99. + // Extension method used to add the middleware to the HTTP request pipeline.
  100. + public static class TimingMiddlewareExtensions
  101. + {
  102. + public static IApplicationBuilder UseTimingMiddleware(this IApplicationBuilder builder)
  103. + {
  104. + return builder.UseMiddleware<TimingMiddleware>();
  105. + }
  106. + }
  107. +}
  108. diff --git a/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs b/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs
  109. index daee31b4f05..7ac04398ed7 100644
  110. --- a/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs
  111. +++ b/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs
  112. @@ -8998,4 +8998,235 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  113. }
  114. }
  115. }
  116. +
  117. + public partial class HttpResponseTrailers
  118. + {
  119. + private static byte[] _headerBytes = new byte[]
  120. + {
  121. + 13,10,69,84,97,103,58,32,
  122. + };
  123. +
  124. + private long _bits = 0;
  125. + private HeaderReferences _headers;
  126. +
  127. +
  128. +
  129. + public StringValues HeaderETag
  130. + {
  131. + get
  132. + {
  133. + StringValues value;
  134. + if ((_bits & 1L) != 0)
  135. + {
  136. + value = _headers._ETag;
  137. + }
  138. + return value;
  139. + }
  140. + set
  141. + {
  142. + _bits |= 1L;
  143. + _headers._ETag = value;
  144. + }
  145. + }
  146. +
  147. + protected override int GetCountFast()
  148. + {
  149. + return (_contentLength.HasValue ? 1 : 0 ) + BitCount(_bits) + (MaybeUnknown?.Count ?? 0);
  150. + }
  151. +
  152. + protected override bool TryGetValueFast(string key, out StringValues value)
  153. + {
  154. + switch (key.Length)
  155. + {
  156. + case 4:
  157. + {
  158. + if ("ETag".Equals(key, StringComparison.OrdinalIgnoreCase))
  159. + {
  160. + if ((_bits & 1L) != 0)
  161. + {
  162. + value = _headers._ETag;
  163. + return true;
  164. + }
  165. + return false;
  166. + }
  167. + }
  168. + break;
  169. + }
  170. +
  171. + return MaybeUnknown?.TryGetValue(key, out value) ?? false;
  172. + }
  173. +
  174. + protected override void SetValueFast(string key, in StringValues value)
  175. + {
  176. + ValidateHeaderValueCharacters(value);
  177. + switch (key.Length)
  178. + {
  179. + case 4:
  180. + {
  181. + if ("ETag".Equals(key, StringComparison.OrdinalIgnoreCase))
  182. + {
  183. + _bits |= 1L;
  184. + _headers._ETag = value;
  185. + return;
  186. + }
  187. + }
  188. + break;
  189. + }
  190. +
  191. + SetValueUnknown(key, value);
  192. + }
  193. +
  194. + protected override bool AddValueFast(string key, in StringValues value)
  195. + {
  196. + ValidateHeaderValueCharacters(value);
  197. + switch (key.Length)
  198. + {
  199. + case 4:
  200. + {
  201. + if ("ETag".Equals(key, StringComparison.OrdinalIgnoreCase))
  202. + {
  203. + if ((_bits & 1L) == 0)
  204. + {
  205. + _bits |= 1L;
  206. + _headers._ETag = value;
  207. + return true;
  208. + }
  209. + return false;
  210. + }
  211. + }
  212. + break;
  213. + }
  214. +
  215. + Unknown.Add(key, value);
  216. + // Return true, above will throw and exit for false
  217. + return true;
  218. + }
  219. +
  220. + protected override bool RemoveFast(string key)
  221. + {
  222. + switch (key.Length)
  223. + {
  224. + case 4:
  225. + {
  226. + if ("ETag".Equals(key, StringComparison.OrdinalIgnoreCase))
  227. + {
  228. + if ((_bits & 1L) != 0)
  229. + {
  230. + _bits &= ~1L;
  231. + _headers._ETag = default(StringValues);
  232. + return true;
  233. + }
  234. + return false;
  235. + }
  236. + }
  237. + break;
  238. + }
  239. +
  240. + return MaybeUnknown?.Remove(key) ?? false;
  241. + }
  242. +
  243. + protected override void ClearFast()
  244. + {
  245. + MaybeUnknown?.Clear();
  246. + _contentLength = null;
  247. + var tempBits = _bits;
  248. + _bits = 0;
  249. + if(HttpHeaders.BitCount(tempBits) > 12)
  250. + {
  251. + _headers = default(HeaderReferences);
  252. + return;
  253. + }
  254. +
  255. + if ((tempBits & 1L) != 0)
  256. + {
  257. + _headers._ETag = default(StringValues);
  258. + if((tempBits & ~1L) == 0)
  259. + {
  260. + return;
  261. + }
  262. + tempBits &= ~1L;
  263. + }
  264. +
  265. + }
  266. +
  267. + protected override bool CopyToFast(KeyValuePair<string, StringValues>[] array, int arrayIndex)
  268. + {
  269. + if (arrayIndex < 0)
  270. + {
  271. + return false;
  272. + }
  273. +
  274. + if ((_bits & 1L) != 0)
  275. + {
  276. + if (arrayIndex == array.Length)
  277. + {
  278. + return false;
  279. + }
  280. + array[arrayIndex] = new KeyValuePair<string, StringValues>("ETag", _headers._ETag);
  281. + ++arrayIndex;
  282. + }
  283. + if (_contentLength.HasValue)
  284. + {
  285. + if (arrayIndex == array.Length)
  286. + {
  287. + return false;
  288. + }
  289. + array[arrayIndex] = new KeyValuePair<string, StringValues>("Content-Length", HeaderUtilities.FormatNonNegativeInt64(_contentLength.Value));
  290. + ++arrayIndex;
  291. + }
  292. + ((ICollection<KeyValuePair<string, StringValues>>)MaybeUnknown)?.CopyTo(array, arrayIndex);
  293. +
  294. + return true;
  295. + }
  296. +
  297. +
  298. +
  299. + private struct HeaderReferences
  300. + {
  301. + public StringValues _ETag;
  302. +
  303. + }
  304. +
  305. + public partial struct Enumerator
  306. + {
  307. + public bool MoveNext()
  308. + {
  309. + switch (_state)
  310. + {
  311. +
  312. + case 0:
  313. + goto state0;
  314. +
  315. + case 1:
  316. + goto state1;
  317. + default:
  318. + goto state_default;
  319. + }
  320. +
  321. + state0:
  322. + if ((_bits & 1L) != 0)
  323. + {
  324. + _current = new KeyValuePair<string, StringValues>("ETag", _collection._headers._ETag);
  325. + _state = 1;
  326. + return true;
  327. + }
  328. +
  329. + state1:
  330. + if (_collection._contentLength.HasValue)
  331. + {
  332. + _current = new KeyValuePair<string, StringValues>("Content-Length", HeaderUtilities.FormatNonNegativeInt64(_collection._contentLength.Value));
  333. + _state = 2;
  334. + return true;
  335. + }
  336. + state_default:
  337. + if (!_hasUnknown || !_unknownEnumerator.MoveNext())
  338. + {
  339. + _current = default(KeyValuePair<string, StringValues>);
  340. + return false;
  341. + }
  342. + _current = _unknownEnumerator.Current;
  343. + return true;
  344. + }
  345. + }
  346. + }
  347. }
  348. \ No newline at end of file
  349. diff --git a/src/Kestrel.Core/Internal/Http/HttpHeaders.cs b/src/Kestrel.Core/Internal/Http/HttpHeaders.cs
  350. index a0a7306d2f1..6acb332a26d 100644
  351. --- a/src/Kestrel.Core/Internal/Http/HttpHeaders.cs
  352. +++ b/src/Kestrel.Core/Internal/Http/HttpHeaders.cs
  353. @@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  354. throw new ArgumentException(CoreStrings.KeyAlreadyExists);
  355. }
  356. - int ICollection<KeyValuePair<string, StringValues>>.Count => GetCountFast();
  357. + public int Count => GetCountFast();
  358. bool ICollection<KeyValuePair<string, StringValues>>.IsReadOnly => _isReadOnly;
  359. diff --git a/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs b/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs
  360. index 7f3b047d70b..51b5daf836b 100644
  361. --- a/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs
  362. +++ b/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs
  363. @@ -209,9 +209,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  364. _currentIHttpUpgradeFeature = this;
  365. }
  366. - protected void ResetIHttp2StreamIdFeature()
  367. + protected void ResetHttp2Features()
  368. {
  369. _currentIHttp2StreamIdFeature = this;
  370. + _currentIHttpResponseTrailersFeature = this;
  371. }
  372. void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state)
  373. diff --git a/src/Kestrel.Core/Internal/Http/HttpProtocol.Generated.cs b/src/Kestrel.Core/Internal/Http/HttpProtocol.Generated.cs
  374. index 03ec02cc8ef..af2b5409623 100644
  375. --- a/src/Kestrel.Core/Internal/Http/HttpProtocol.Generated.cs
  376. +++ b/src/Kestrel.Core/Internal/Http/HttpProtocol.Generated.cs
  377. @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  378. private static readonly Type IFormFeatureType = typeof(IFormFeature);
  379. private static readonly Type IHttpUpgradeFeatureType = typeof(IHttpUpgradeFeature);
  380. private static readonly Type IHttp2StreamIdFeatureType = typeof(IHttp2StreamIdFeature);
  381. + private static readonly Type IHttpResponseTrailersFeatureType = typeof(IHttpResponseTrailersFeature);
  382. private static readonly Type IResponseCookiesFeatureType = typeof(IResponseCookiesFeature);
  383. private static readonly Type IItemsFeatureType = typeof(IItemsFeature);
  384. private static readonly Type ITlsConnectionFeatureType = typeof(ITlsConnectionFeature);
  385. @@ -46,6 +47,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  386. private object _currentIFormFeature;
  387. private object _currentIHttpUpgradeFeature;
  388. private object _currentIHttp2StreamIdFeature;
  389. + private object _currentIHttpResponseTrailersFeature;
  390. private object _currentIResponseCookiesFeature;
  391. private object _currentIItemsFeature;
  392. private object _currentITlsConnectionFeature;
  393. @@ -79,6 +81,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  394. _currentIFormFeature = null;
  395. _currentIHttpUpgradeFeature = null;
  396. _currentIHttp2StreamIdFeature = null;
  397. + _currentIHttpResponseTrailersFeature = null;
  398. _currentIResponseCookiesFeature = null;
  399. _currentIItemsFeature = null;
  400. _currentITlsConnectionFeature = null;
  401. @@ -183,6 +186,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  402. {
  403. feature = _currentIHttp2StreamIdFeature;
  404. }
  405. + else if (key == IHttpResponseTrailersFeatureType)
  406. + {
  407. + feature = _currentIHttpResponseTrailersFeature;
  408. + }
  409. else if (key == IResponseCookiesFeatureType)
  410. {
  411. feature = _currentIResponseCookiesFeature;
  412. @@ -279,6 +286,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  413. {
  414. _currentIHttp2StreamIdFeature = value;
  415. }
  416. + else if (key == IHttpResponseTrailersFeatureType)
  417. + {
  418. + _currentIHttpResponseTrailersFeature = value;
  419. + }
  420. else if (key == IResponseCookiesFeatureType)
  421. {
  422. _currentIResponseCookiesFeature = value;
  423. @@ -373,6 +384,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  424. {
  425. feature = (TFeature)_currentIHttp2StreamIdFeature;
  426. }
  427. + else if (typeof(TFeature) == typeof(IHttpResponseTrailersFeature))
  428. + {
  429. + feature = (TFeature)_currentIHttpResponseTrailersFeature;
  430. + }
  431. else if (typeof(TFeature) == typeof(IResponseCookiesFeature))
  432. {
  433. feature = (TFeature)_currentIResponseCookiesFeature;
  434. @@ -473,6 +488,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  435. {
  436. _currentIHttp2StreamIdFeature = feature;
  437. }
  438. + else if (typeof(TFeature) == typeof(IHttpResponseTrailersFeature))
  439. + {
  440. + _currentIHttpResponseTrailersFeature = feature;
  441. + }
  442. else if (typeof(TFeature) == typeof(IResponseCookiesFeature))
  443. {
  444. _currentIResponseCookiesFeature = feature;
  445. @@ -565,6 +584,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  446. {
  447. yield return new KeyValuePair<Type, object>(IHttp2StreamIdFeatureType, _currentIHttp2StreamIdFeature);
  448. }
  449. + if (_currentIHttpResponseTrailersFeature != null)
  450. + {
  451. + yield return new KeyValuePair<Type, object>(IHttpResponseTrailersFeatureType, _currentIHttpResponseTrailersFeature);
  452. + }
  453. if (_currentIResponseCookiesFeature != null)
  454. {
  455. yield return new KeyValuePair<Type, object>(IResponseCookiesFeatureType, _currentIResponseCookiesFeature);
  456. diff --git a/src/Kestrel.Core/Internal/Http/HttpResponseTrailers.cs b/src/Kestrel.Core/Internal/Http/HttpResponseTrailers.cs
  457. new file mode 100644
  458. index 00000000000..4e910e31a66
  459. --- /dev/null
  460. +++ b/src/Kestrel.Core/Internal/Http/HttpResponseTrailers.cs
  461. @@ -0,0 +1,65 @@
  462. +// Copyright (c) .NET Foundation. All rights reserved.
  463. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  464. +
  465. +using System.Collections;
  466. +using System.Collections.Generic;
  467. +using System.Runtime.CompilerServices;
  468. +using Microsoft.Extensions.Primitives;
  469. +
  470. +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  471. +{
  472. + public partial class HttpResponseTrailers : HttpHeaders
  473. + {
  474. + public Enumerator GetEnumerator()
  475. + {
  476. + return new Enumerator(this);
  477. + }
  478. +
  479. + protected override IEnumerator<KeyValuePair<string, StringValues>> GetEnumeratorFast()
  480. + {
  481. + return GetEnumerator();
  482. + }
  483. +
  484. + [MethodImpl(MethodImplOptions.NoInlining)]
  485. + private void SetValueUnknown(string key, in StringValues value)
  486. + {
  487. + ValidateHeaderNameCharacters(key);
  488. + Unknown[key] = value;
  489. + }
  490. +
  491. + public partial struct Enumerator : IEnumerator<KeyValuePair<string, StringValues>>
  492. + {
  493. + private readonly HttpResponseTrailers _collection;
  494. + private readonly long _bits;
  495. + private int _state;
  496. + private KeyValuePair<string, StringValues> _current;
  497. + private readonly bool _hasUnknown;
  498. + private Dictionary<string, StringValues>.Enumerator _unknownEnumerator;
  499. +
  500. + internal Enumerator(HttpResponseTrailers collection)
  501. + {
  502. + _collection = collection;
  503. + _bits = collection._bits;
  504. + _state = 0;
  505. + _current = default;
  506. + _hasUnknown = collection.MaybeUnknown != null;
  507. + _unknownEnumerator = _hasUnknown
  508. + ? collection.MaybeUnknown.GetEnumerator()
  509. + : default;
  510. + }
  511. +
  512. + public KeyValuePair<string, StringValues> Current => _current;
  513. +
  514. + object IEnumerator.Current => _current;
  515. +
  516. + public void Dispose()
  517. + {
  518. + }
  519. +
  520. + public void Reset()
  521. + {
  522. + _state = 0;
  523. + }
  524. + }
  525. + }
  526. +}
  527. diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
  528. index 87c6c4ed3b6..e3968fc14df 100644
  529. --- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
  530. +++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
  531. @@ -90,7 +90,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
  532. var http2Limits = httpLimits.Http2;
  533. _context = context;
  534. - _frameWriter = new Http2FrameWriter(context.Transport.Output, context.ConnectionContext, _outputFlowControl, context.TimeoutControl, context.ConnectionId, context.ServiceContext.Log);
  535. + _frameWriter = new Http2FrameWriter(context.Transport.Output, context.ConnectionContext, this, _outputFlowControl, context.TimeoutControl, context.ConnectionId, context.ServiceContext.Log);
  536. _serverSettings.MaxConcurrentStreams = (uint)http2Limits.MaxStreamsPerConnection;
  537. _serverSettings.MaxFrameSize = (uint)http2Limits.MaxFrameSize;
  538. _serverSettings.HeaderTableSize = (uint)http2Limits.HeaderTableSize;
  539. diff --git a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs
  540. index f66f1627072..cd8bc7ae482 100644
  541. --- a/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs
  542. +++ b/src/Kestrel.Core/Internal/Http2/Http2FrameWriter.cs
  543. @@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
  544. private readonly PipeWriter _outputWriter;
  545. private bool _aborted;
  546. private readonly ConnectionContext _connectionContext;
  547. + private readonly Http2Connection _http2Connection;
  548. private readonly OutputFlowControl _connectionOutputFlowControl;
  549. private readonly string _connectionId;
  550. private readonly IKestrelTrace _log;
  551. @@ -41,6 +42,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
  552. public Http2FrameWriter(
  553. PipeWriter outputPipeWriter,
  554. ConnectionContext connectionContext,
  555. + Http2Connection http2Connection,
  556. OutputFlowControl connectionOutputFlowControl,
  557. ITimeoutControl timeoutControl,
  558. string connectionId,
  559. @@ -48,6 +50,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
  560. {
  561. _outputWriter = outputPipeWriter;
  562. _connectionContext = connectionContext;
  563. + _http2Connection = http2Connection;
  564. _connectionOutputFlowControl = connectionOutputFlowControl;
  565. _connectionId = connectionId;
  566. _log = log;
  567. @@ -157,39 +160,69 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
  568. _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId);
  569. var buffer = _headerEncodingBuffer.AsSpan();
  570. var done = _hpackEncoder.BeginEncode(statusCode, EnumerateHeaders(headers), buffer, out var payloadLength);
  571. + FinishWritingHeaders(streamId, payloadLength, done);
  572. + }
  573. + catch (HPackEncodingException hex)
  574. + {
  575. + _log.HPackEncodingError(_connectionId, streamId, hex);
  576. + _http2Connection.Abort(new ConnectionAbortedException(hex.Message, hex));
  577. + throw new InvalidOperationException(hex.Message, hex); // Report the error to the user if this was the first write.
  578. + }
  579. + }
  580. + }
  581. - _outgoingFrame.PayloadLength = payloadLength;
  582. + public Task WriteResponseTrailers(int streamId, HttpResponseTrailers headers)
  583. + {
  584. + lock (_writeLock)
  585. + {
  586. + if (_completed)
  587. + {
  588. + return Task.CompletedTask;
  589. + }
  590. - if (done)
  591. - {
  592. - _outgoingFrame.HeadersFlags = Http2HeadersFrameFlags.END_HEADERS;
  593. - }
  594. + try
  595. + {
  596. + _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_STREAM, streamId);
  597. + var buffer = _headerEncodingBuffer.AsSpan();
  598. + var done = _hpackEncoder.BeginEncode(EnumerateHeaders(headers), buffer, out var payloadLength);
  599. + FinishWritingHeaders(streamId, payloadLength, done);
  600. + }
  601. + catch (HPackEncodingException hex)
  602. + {
  603. + _log.HPackEncodingError(_connectionId, streamId, hex);
  604. + _http2Connection.Abort(new ConnectionAbortedException(hex.Message, hex));
  605. + }
  606. - WriteHeaderUnsynchronized();
  607. - _outputWriter.Write(buffer.Slice(0, payloadLength));
  608. + return _flusher.FlushAsync();
  609. + }
  610. + }
  611. - while (!done)
  612. - {
  613. - _outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId);
  614. + private void FinishWritingHeaders(int streamId, int payloadLength, bool done)
  615. + {
  616. + var buffer = _headerEncodingBuffer.AsSpan();
  617. + _outgoingFrame.PayloadLength = payloadLength;
  618. + if (done)
  619. + {
  620. + _outgoingFrame.HeadersFlags |= Http2HeadersFrameFlags.END_HEADERS;
  621. + }
  622. - done = _hpackEncoder.Encode(buffer, out payloadLength);
  623. - _outgoingFrame.PayloadLength = payloadLength;
  624. + WriteHeaderUnsynchronized();
  625. + _outputWriter.Write(buffer.Slice(0, payloadLength));
  626. - if (done)
  627. - {
  628. - _outgoingFrame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS;
  629. - }
  630. + while (!done)
  631. + {
  632. + _outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId);
  633. - WriteHeaderUnsynchronized();
  634. - _outputWriter.Write(buffer.Slice(0, payloadLength));
  635. - }
  636. - }
  637. - catch (HPackEncodingException hex)
  638. + done = _hpackEncoder.Encode(buffer, out payloadLength);
  639. + _outgoingFrame.PayloadLength = payloadLength;
  640. +
  641. + if (done)
  642. {
  643. - // Header errors are fatal to the connection. We don't have a direct way to signal this to the Http2Connection.
  644. - _connectionContext.Abort(new ConnectionAbortedException("", hex));
  645. - throw new InvalidOperationException("", hex); // Report the error to the user if this was the first write.
  646. + _outgoingFrame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS;
  647. }
  648. +
  649. + WriteHeaderUnsynchronized();
  650. + _outputWriter.Write(buffer.Slice(0, payloadLength));
  651. }
  652. }
  653. diff --git a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs
  654. index 812a2d9fca9..1485b65681a 100644
  655. --- a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs
  656. +++ b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs
  657. @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
  658. // This should only be accessed via the FrameWriter. The connection-level output flow control is protected by the
  659. // FrameWriter's connection-level write lock.
  660. private readonly StreamOutputFlowControl _flowControl;
  661. -
  662. + private readonly Http2Stream _stream;
  663. private readonly object _dataWriterLock = new object();
  664. private readonly Pipe _dataPipe;
  665. private readonly Task _dataWriteProcessingTask;
  666. @@ -37,11 +37,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
  667. Http2FrameWriter frameWriter,
  668. StreamOutputFlowControl flowControl,
  669. ITimeoutControl timeoutControl,
  670. - MemoryPool<byte> pool)
  671. + MemoryPool<byte> pool,
  672. + Http2Stream stream)
  673. {
  674. _streamId = streamId;
  675. _frameWriter = frameWriter;
  676. _flowControl = flowControl;
  677. + _stream = stream;
  678. _dataPipe = CreateDataPipe(pool);
  679. _flusher = new TimingPipeFlusher(_dataPipe.Writer, timeoutControl);
  680. _dataWriteProcessingTask = ProcessDataWrites();
  681. @@ -200,7 +202,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
  682. {
  683. readResult = await _dataPipe.Reader.ReadAsync();
  684. - await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: readResult.IsCompleted);
  685. + if (readResult.IsCompleted && _stream.Trailers?.Count > 0)
  686. + {
  687. + if (readResult.Buffer.Length > 0)
  688. + {
  689. + await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: false);
  690. + }
  691. +
  692. + await _frameWriter.WriteResponseTrailers(_streamId, _stream.Trailers);
  693. + }
  694. + else
  695. + {
  696. + await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: readResult.IsCompleted);
  697. + }
  698. _dataPipe.Reader.AdvanceTo(readResult.Buffer.End);
  699. } while (!readResult.IsCompleted);
  700. diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.FeatureCollection.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.FeatureCollection.cs
  701. index 782a8ddf51d..72604950585 100644
  702. --- a/src/Kestrel.Core/Internal/Http2/Http2Stream.FeatureCollection.cs
  703. +++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.FeatureCollection.cs
  704. @@ -1,12 +1,34 @@
  705. // Copyright (c) .NET Foundation. All rights reserved.
  706. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  707. +using Microsoft.AspNetCore.Http;
  708. +using Microsoft.AspNetCore.Http.Features;
  709. using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
  710. +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
  711. namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
  712. {
  713. - public partial class Http2Stream : IHttp2StreamIdFeature
  714. + public partial class Http2Stream : IHttp2StreamIdFeature, IHttpResponseTrailersFeature
  715. {
  716. + internal HttpResponseTrailers Trailers { get; set; }
  717. + private IHeaderDictionary _userTrailers;
  718. +
  719. + IHeaderDictionary IHttpResponseTrailersFeature.Trailers
  720. + {
  721. + get
  722. + {
  723. + if (Trailers == null)
  724. + {
  725. + Trailers = new HttpResponseTrailers();
  726. + }
  727. + return _userTrailers ?? Trailers;
  728. + }
  729. + set
  730. + {
  731. + _userTrailers = value;
  732. + }
  733. + }
  734. +
  735. int IHttp2StreamIdFeature.StreamId => _context.StreamId;
  736. }
  737. }
  738. diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
  739. index be7c9b9d091..79d93b2a4c9 100644
  740. --- a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
  741. +++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs
  742. @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
  743. _context.ServerPeerSettings.InitialWindowSize / 2);
  744. _outputFlowControl = new StreamOutputFlowControl(context.ConnectionOutputFlowControl, context.ClientPeerSettings.InitialWindowSize);
  745. - _http2Output = new Http2OutputProducer(context.StreamId, context.FrameWriter, _outputFlowControl, context.TimeoutControl, context.MemoryPool);
  746. + _http2Output = new Http2OutputProducer(context.StreamId, context.FrameWriter, _outputFlowControl, context.TimeoutControl, context.MemoryPool, this);
  747. RequestBodyPipe = CreateRequestBodyPipe(_context.ServerPeerSettings.InitialWindowSize);
  748. Output = _http2Output;
  749. @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
  750. protected override void OnReset()
  751. {
  752. - ResetIHttp2StreamIdFeature();
  753. + ResetHttp2Features();
  754. }
  755. protected override void OnRequestProcessingEnded()
  756. diff --git a/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs b/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
  757. index 19ecc86ee2b..852fea7953d 100644
  758. --- a/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
  759. +++ b/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs
  760. @@ -67,6 +67,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
  761. void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex);
  762. + void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex);
  763. +
  764. void Http2FrameReceived(string connectionId, Http2Frame frame);
  765. void Http2FrameSending(string connectionId, Http2Frame frame);
  766. diff --git a/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs b/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
  767. index f735370d3e4..39bf9d2895e 100644
  768. --- a/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
  769. +++ b/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs
  770. @@ -107,6 +107,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
  771. LoggerMessage.Define<string, Http2FrameType, int, int, object>(LogLevel.Trace, new EventId(37, nameof(Http2FrameReceived)),
  772. @"Connection id ""{ConnectionId}"" sending {type} frame for stream ID {id} with length {length} and flags {flags}");
  773. + private static readonly Action<ILogger, string, int, Exception> _hpackEncodingError =
  774. + LoggerMessage.Define<string, int>(LogLevel.Information, new EventId(38, nameof(HPackEncodingError)),
  775. + @"Connection id ""{ConnectionId}"": HPACK encoding error while encoding headers for stream ID {StreamId}.");
  776. +
  777. protected readonly ILogger _logger;
  778. public KestrelTrace(ILogger logger)
  779. @@ -254,6 +258,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
  780. _hpackDecodingError(_logger, connectionId, streamId, ex);
  781. }
  782. + public virtual void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex)
  783. + {
  784. + _hpackEncodingError(_logger, connectionId, streamId, ex);
  785. + }
  786. +
  787. public void Http2FrameReceived(string connectionId, Http2Frame frame)
  788. {
  789. _http2FrameReceived(_logger, connectionId, frame.Type, frame.StreamId, frame.PayloadLength, frame.ShowFlags(), null);
  790. diff --git a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs
  791. index 50d52558543..74912e65b6a 100644
  792. --- a/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs
  793. +++ b/test/Kestrel.InMemory.FunctionalTests/Http2/Http2StreamTests.cs
  794. @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Http;
  795. using Microsoft.AspNetCore.Http.Features;
  796. using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
  797. using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
  798. +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
  799. using Microsoft.Extensions.Logging;
  800. using Microsoft.Net.Http.Headers;
  801. using Xunit;
  802. @@ -1340,6 +1341,223 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
  803. Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
  804. }
  805. + [Fact]
  806. + public async Task ResponseTrailers_WithoutData_Sent()
  807. + {
  808. + await InitializeConnectionAsync(context =>
  809. + {
  810. + context.Response.AppendTrailer("CustomName", "Custom Value");
  811. + return Task.CompletedTask;
  812. + });
  813. +
  814. + await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
  815. +
  816. + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
  817. + withLength: 55,
  818. + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
  819. + withStreamId: 1);
  820. +
  821. + var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
  822. + withLength: 25,
  823. + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
  824. + withStreamId: 1);
  825. +
  826. + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
  827. +
  828. + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
  829. +
  830. + Assert.Equal(3, _decodedHeaders.Count);
  831. + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
  832. + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
  833. + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
  834. +
  835. + _decodedHeaders.Clear();
  836. +
  837. + _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
  838. +
  839. + Assert.Single(_decodedHeaders);
  840. + Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
  841. + }
  842. +
  843. + [Fact]
  844. + public async Task ResponseTrailers_WithData_Sent()
  845. + {
  846. + await InitializeConnectionAsync(async context =>
  847. + {
  848. + await context.Response.WriteAsync("Hello World");
  849. + context.Response.AppendTrailer("CustomName", "Custom Value");
  850. + });
  851. +
  852. + await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
  853. +
  854. + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
  855. + withLength: 37,
  856. + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
  857. + withStreamId: 1);
  858. +
  859. + await ExpectAsync(Http2FrameType.DATA,
  860. + withLength: 11,
  861. + withFlags: (byte)Http2DataFrameFlags.NONE,
  862. + withStreamId: 1);
  863. +
  864. + var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
  865. + withLength: 25,
  866. + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
  867. + withStreamId: 1);
  868. +
  869. + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
  870. +
  871. + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
  872. +
  873. + Assert.Equal(2, _decodedHeaders.Count);
  874. + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
  875. + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
  876. +
  877. + _decodedHeaders.Clear();
  878. +
  879. + _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
  880. +
  881. + Assert.Single(_decodedHeaders);
  882. + Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
  883. + }
  884. +
  885. + [Fact]
  886. + public async Task ResponseTrailers_WithContinuation_Sent()
  887. + {
  888. + var largeHeader = new string('a', 1024 * 3);
  889. + await InitializeConnectionAsync(async context =>
  890. + {
  891. + await context.Response.WriteAsync("Hello World");
  892. + // The first five fill the first frame
  893. + context.Response.AppendTrailer("CustomName0", largeHeader);
  894. + context.Response.AppendTrailer("CustomName1", largeHeader);
  895. + context.Response.AppendTrailer("CustomName2", largeHeader);
  896. + context.Response.AppendTrailer("CustomName3", largeHeader);
  897. + context.Response.AppendTrailer("CustomName4", largeHeader);
  898. + // This one spills over to the next frame
  899. + context.Response.AppendTrailer("CustomName5", largeHeader);
  900. + });
  901. +
  902. + await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
  903. +
  904. + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
  905. + withLength: 37,
  906. + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
  907. + withStreamId: 1);
  908. +
  909. + await ExpectAsync(Http2FrameType.DATA,
  910. + withLength: 11,
  911. + withFlags: (byte)Http2DataFrameFlags.NONE,
  912. + withStreamId: 1);
  913. +
  914. + var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
  915. + withLength: 15440,
  916. + withFlags: (byte)Http2HeadersFrameFlags.END_STREAM,
  917. + withStreamId: 1);
  918. +
  919. + var trailersContinuationFrame = await ExpectAsync(Http2FrameType.CONTINUATION,
  920. + withLength: 3088,
  921. + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
  922. + withStreamId: 1);
  923. +
  924. + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
  925. +
  926. + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
  927. +
  928. + Assert.Equal(2, _decodedHeaders.Count);
  929. + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
  930. + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
  931. +
  932. + _decodedHeaders.Clear();
  933. +
  934. + _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: false, handler: this);
  935. +
  936. + Assert.Equal(5, _decodedHeaders.Count);
  937. + Assert.Equal(largeHeader, _decodedHeaders["CustomName0"]);
  938. + Assert.Equal(largeHeader, _decodedHeaders["CustomName1"]);
  939. + Assert.Equal(largeHeader, _decodedHeaders["CustomName2"]);
  940. + Assert.Equal(largeHeader, _decodedHeaders["CustomName3"]);
  941. + Assert.Equal(largeHeader, _decodedHeaders["CustomName4"]);
  942. +
  943. + _decodedHeaders.Clear();
  944. +
  945. + _hpackDecoder.Decode(trailersContinuationFrame.PayloadSequence, endHeaders: true, handler: this);
  946. +
  947. + Assert.Single(_decodedHeaders);
  948. + Assert.Equal(largeHeader, _decodedHeaders["CustomName5"]);
  949. + }
  950. +
  951. + [Fact]
  952. + public async Task ResponseTrailers_WithNonAscii_Throws()
  953. + {
  954. + await InitializeConnectionAsync(async context =>
  955. + {
  956. + await context.Response.WriteAsync("Hello World");
  957. + Assert.Throws<InvalidOperationException>(() => context.Response.AppendTrailer("Custom你好Name", "Custom Value"));
  958. + Assert.Throws<InvalidOperationException>(() => context.Response.AppendTrailer("CustomName", "Custom 你好 Value"));
  959. + });
  960. +
  961. + await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
  962. +
  963. + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
  964. + withLength: 37,
  965. + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
  966. + withStreamId: 1);
  967. +
  968. + await ExpectAsync(Http2FrameType.DATA,
  969. + withLength: 11,
  970. + withFlags: (byte)Http2DataFrameFlags.NONE,
  971. + withStreamId: 1);
  972. +
  973. + await ExpectAsync(Http2FrameType.DATA,
  974. + withLength: 0,
  975. + withFlags: (byte)Http2DataFrameFlags.END_STREAM,
  976. + withStreamId: 1);
  977. +
  978. + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
  979. +
  980. + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
  981. +
  982. + Assert.Equal(2, _decodedHeaders.Count);
  983. + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
  984. + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
  985. + }
  986. +
  987. + [Fact]
  988. + public async Task ResponseTrailers_TooLong_Throws()
  989. + {
  990. + await InitializeConnectionAsync(async context =>
  991. + {
  992. + await context.Response.WriteAsync("Hello World");
  993. + context.Response.AppendTrailer("too_long", new string('a', (int)Http2PeerSettings.DefaultMaxFrameSize));
  994. + });
  995. +
  996. + await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
  997. +
  998. + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
  999. + withLength: 37,
  1000. + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
  1001. + withStreamId: 1);
  1002. +
  1003. + await ExpectAsync(Http2FrameType.DATA,
  1004. + withLength: 11,
  1005. + withFlags: (byte)Http2DataFrameFlags.NONE,
  1006. + withStreamId: 1);
  1007. +
  1008. + var goAway = await ExpectAsync(Http2FrameType.GOAWAY,
  1009. + withLength: 8,
  1010. + withFlags: (byte)Http2DataFrameFlags.NONE,
  1011. + withStreamId: 0);
  1012. +
  1013. + VerifyGoAway(goAway, 1, Http2ErrorCode.INTERNAL_ERROR);
  1014. +
  1015. + _pair.Application.Output.Complete();
  1016. + await _connectionTask;
  1017. +
  1018. + var message = Assert.Single(TestApplicationErrorLogger.Messages, m => m.Exception is HPackEncodingException);
  1019. + Assert.Contains(CoreStrings.HPackErrorNotEnoughBuffer, message.Exception.Message);
  1020. + }
  1021. +
  1022. [Fact]
  1023. public async Task ApplicationException_BeforeFirstWrite_Sends500()
  1024. {
  1025. @@ -1750,6 +1968,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
  1026. var message = await appFinished.Task.DefaultTimeout();
  1027. Assert.Equal(CoreStrings.HPackErrorNotEnoughBuffer, message);
  1028. +
  1029. + // Just the StatusCode gets written before aborting in the continuation frame
  1030. + await ExpectAsync(Http2FrameType.HEADERS,
  1031. + withLength: 37,
  1032. + withFlags: (byte)Http2HeadersFrameFlags.NONE,
  1033. + withStreamId: 1);
  1034. +
  1035. + _pair.Application.Output.Complete();
  1036. +
  1037. + await WaitForConnectionErrorAsync<HPackEncodingException>(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR,
  1038. + CoreStrings.HPackErrorNotEnoughBuffer);
  1039. }
  1040. }
  1041. }
  1042. \ No newline at end of file
  1043. diff --git a/test/shared/CompositeKestrelTrace.cs b/test/shared/CompositeKestrelTrace.cs
  1044. index 8c9be6c1bc2..486db3987a2 100644
  1045. --- a/test/shared/CompositeKestrelTrace.cs
  1046. +++ b/test/shared/CompositeKestrelTrace.cs
  1047. @@ -189,6 +189,12 @@ namespace Microsoft.AspNetCore.Testing
  1048. _trace2.HPackDecodingError(connectionId, streamId, ex);
  1049. }
  1050. + public void HPackEncodingError(string connectionId, int streamId, HPackEncodingException ex)
  1051. + {
  1052. + _trace1.HPackEncodingError(connectionId, streamId, ex);
  1053. + _trace2.HPackEncodingError(connectionId, streamId, ex);
  1054. + }
  1055. +
  1056. public void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason)
  1057. {
  1058. _trace1.Http2StreamResetAbort(traceIdentifier, error, abortReason);
  1059. diff --git a/tools/CodeGenerator/HttpProtocolFeatureCollection.cs b/tools/CodeGenerator/HttpProtocolFeatureCollection.cs
  1060. index 61025768323..85e41da0fbd 100644
  1061. --- a/tools/CodeGenerator/HttpProtocolFeatureCollection.cs
  1062. +++ b/tools/CodeGenerator/HttpProtocolFeatureCollection.cs
  1063. @@ -30,6 +30,7 @@ namespace CodeGenerator
  1064. {
  1065. "IHttpUpgradeFeature",
  1066. "IHttp2StreamIdFeature",
  1067. + "IHttpResponseTrailersFeature",
  1068. "IResponseCookiesFeature",
  1069. "IItemsFeature",
  1070. "ITlsConnectionFeature",
  1071. diff --git a/tools/CodeGenerator/KnownHeaders.cs b/tools/CodeGenerator/KnownHeaders.cs
  1072. index 5e16debf1df..2461796c17e 100644
  1073. --- a/tools/CodeGenerator/KnownHeaders.cs
  1074. +++ b/tools/CodeGenerator/KnownHeaders.cs
  1075. @@ -269,6 +269,21 @@ namespace CodeGenerator
  1076. PrimaryHeader = responsePrimaryHeaders.Contains("Content-Length")
  1077. }})
  1078. .ToArray();
  1079. +
  1080. + var responseTrailers = new[]
  1081. + {
  1082. + "ETag",
  1083. + }
  1084. + .Select((header, index) => new KnownHeader
  1085. + {
  1086. + Name = header,
  1087. + Index = index,
  1088. + EnhancedSetter = enhancedHeaders.Contains(header),
  1089. + ExistenceCheck = responseHeadersExistence.Contains(header),
  1090. + PrimaryHeader = responsePrimaryHeaders.Contains(header)
  1091. + })
  1092. + .ToArray();
  1093. +
  1094. // 63 for responseHeaders as it steals one bit for Content-Length in CopyTo(ref MemoryPoolIterator output)
  1095. Debug.Assert(responseHeaders.Length <= 63);
  1096. Debug.Assert(responseHeaders.Max(x => x.Index) <= 62);
  1097. @@ -288,6 +303,13 @@ namespace CodeGenerator
  1098. HeadersByLength = responseHeaders.GroupBy(x => x.Name.Length),
  1099. ClassName = "HttpResponseHeaders",
  1100. Bytes = responseHeaders.SelectMany(header => header.Bytes).ToArray()
  1101. + },
  1102. + new
  1103. + {
  1104. + Headers = responseTrailers,
  1105. + HeadersByLength = responseTrailers.GroupBy(x => x.Name.Length),
  1106. + ClassName = "HttpResponseTrailers",
  1107. + Bytes = responseTrailers.SelectMany(header => header.Bytes).ToArray()
  1108. }
  1109. };
  1110. foreach (var loop in loops.Where(l => l.Bytes != null))
  1111. @@ -402,7 +424,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  1112. }}
  1113. protected override void SetValueFast(string key, in StringValues value)
  1114. - {{{(loop.ClassName == "HttpResponseHeaders" ? @"
  1115. + {{{(loop.ClassName != "HttpRequestHeaders" ? @"
  1116. ValidateHeaderValueCharacters(value);" : "")}
  1117. switch (key.Length)
  1118. {{{Each(loop.HeadersByLength, byLength => $@"
  1119. @@ -424,7 +446,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
  1120. }}
  1121. protected override bool AddValueFast(string key, in StringValues value)
  1122. - {{{(loop.ClassName == "HttpResponseHeaders" ? @"
  1123. + {{{(loop.ClassName != "HttpRequestHeaders" ? @"
  1124. ValidateHeaderValueCharacters(value);" : "")}
  1125. switch (key.Length)
  1126. {{{Each(loop.HeadersByLength, byLength => $@"