Skip to content

Commit a939468

Browse files
author
ladeak
committed
Http trailers feature
1 parent 98f4e78 commit a939468

4 files changed

Lines changed: 126 additions & 8 deletions

File tree

src/CHttpServer/CHttpServer/Http3/Http3Stream.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@ public Http3Stream(FeatureCollection features)
3939
_streamCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously);
4040
_requestHeaders = [];
4141
_responseHeaders = [];
42+
_responseTrailers = [];
4243

4344
_features = features;
4445
_features.AddRange(
4546
(typeof(IHttpRequestFeature), this),
4647
(typeof(IHttpResponseFeature), this),
47-
(typeof(IHttpResponseBodyFeature), this));
48-
//_features.Add<IHttpResponseTrailersFeature>(this);
48+
(typeof(IHttpResponseBodyFeature), this),
49+
(typeof(IHttpResponseTrailersFeature), this));
4950
//_features.Add<IHttpRequestBodyDetectionFeature>(this);
5051
//_features.Add<IHttpRequestLifetimeFeature>(this);
5152
//_features.Add<IPriority9218Feature>(this);
@@ -71,6 +72,7 @@ public void Initialize(Http3Connection connection, QuicStream quicStream)
7172
_features.ResetCheckpoint();
7273
_requestHeaders.ResetHeaderCollection();
7374
_responseHeaders.ResetHeaderCollection();
75+
_responseTrailers.ResetHeaderCollection();
7476
StatusCode = 200;
7577
_hasStarted = 0;
7678
_onStartingCallback = null;
@@ -242,12 +244,14 @@ private async Task StartApplicationProcessing<TContext>(IHttpApplication<TContex
242244
await StartAsync(token);
243245

244246
// Write trailers
245-
//await WriteHeadersAsync(null); // todo trailers features
247+
_responseTrailers.SetReadOnly();
248+
await WriteHeadersAsync(_responseTrailers); // todo trailers features
246249
_quicStream?.CompleteWrites();
247250
}
248251
catch (Exception)
249252
{
250-
// TODO, close stream, ...
253+
_quicStream?.Dispose();
254+
_quicStream = null;
251255
}
252256
}
253257

@@ -262,5 +266,10 @@ private async Task WriteHeadersAsync(Http3ResponseHeaderCollection headers)
262266
_qpackDecoder.Encode(headers, _responseHeaderWriter);
263267
await _responseHeaderWriter.FlushAsync();
264268
}
265-
266269
}
270+
271+
internal partial class Http3Stream : IHttpResponseTrailersFeature
272+
{
273+
private readonly Http3ResponseHeaderCollection _responseTrailers;
274+
public IHeaderDictionary Trailers { get => _responseTrailers; set => throw new PlatformNotSupportedException(); }
275+
}

src/CHttpServer/CHttpServer/Http3/QPackStaticTable.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal static class QPackStaticTable
1313
CreateHeaderField(4, "Content-Length", "0"),
1414
CreateHeaderField(5, "Cookie", ""),
1515
CreateHeaderField(6, "Date", ""),
16-
CreateHeaderField(7, "Etag", ""),
16+
CreateHeaderField(7, "ETag", ""),
1717
CreateHeaderField(8, "If-Modified-Since", ""),
1818
CreateHeaderField(9, "If-None-Match", ""),
1919
CreateHeaderField(10, "Last-Modified", ""),
@@ -92,7 +92,7 @@ internal static class QPackStaticTable
9292
CreateHeaderField(83, "Alt-Svc", ""),
9393
CreateHeaderField(84, "Authorization", ""),
9494
CreateHeaderField(85, "Content-Security-Policy", "script-src 'none'; object-src 'none'; base-uri 'none'"),
95-
CreateHeaderField(86, "Early-Data", "1"),
95+
CreateHeaderField(86, "early-data", "1"),
9696
CreateHeaderField(87, "expect-ct", ""),
9797
CreateHeaderField(88, "forwarded", ""),
9898
CreateHeaderField(89, "If-Range", ""),

tests/CHttpServer.Tests/Http3/Http3IntegrationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public async Task SmallHeaderRequestResponse()
9595
Assert.True(response.Headers.TryGetValues(headerName1, out var values1) && values1.Single() == headerValue1);
9696
}
9797

98-
[Fact(Skip = "WIP")]
98+
[Fact]
9999
public async Task Header_And_Trailers()
100100
{
101101
var client = CreateClient();

tests/CHttpServer.Tests/Http3/QPackDecoderTests.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,115 @@ public void Decode_Mix_DifferentHeaderOrder()
275275
Assert.Equal("longervalue", testHandler.Headers["longerheader"]);
276276
}
277277

278+
[Fact]
279+
public void Decode_ETag()
280+
{
281+
// 0, 0 - prefix, 0x07 - ETag, 0x40 - field line and name reference, 0x10 - static table, 0x01 - length, 0x61 - 'a'
282+
byte[] data = [0x00, 0x00, 0x07 + 0x40 + 0x10, 0x01, 0x61];
283+
for (int i = 1; i < data.Length - 1; i++)
284+
{
285+
QPackDecoder decoder = new();
286+
TestQPackHeaderHandler testHandler = new();
287+
var success = decoder.DecodeHeader(new ReadOnlySequence<byte>(data.AsMemory(0, i)), testHandler, out long consumed);
288+
success = decoder.DecodeHeader(new ReadOnlySequence<byte>(data.AsMemory(checked((int)consumed))), testHandler, out long finalConsumed);
289+
Assert.True(success);
290+
Assert.Equal(data.Length, consumed + finalConsumed);
291+
Assert.Single(testHandler.Headers);
292+
Assert.Equal("a", testHandler.Headers[HeaderNames.ETag]);
293+
}
294+
}
295+
296+
[Theory]
297+
[MemberData(nameof(KnownAspNetHeaders))]
298+
public void Decode_AspNetHeaders(string headerName, byte staticTableIndex)
299+
{
300+
// 0, 0 - prefix, table index, 0x40 - field line and name reference, 0x10 - static table, 0x01 - length, 0x61 - 'a'
301+
Span<byte> encodedIndex = new byte[8];
302+
int length = QPackIntegerEncoder.Encode(encodedIndex, staticTableIndex, 4);
303+
encodedIndex[0] += 0x40 + 0x10;
304+
305+
byte[] data = [0x00, 0x00, .. encodedIndex[..length], 0x01, 0x61];
306+
QPackDecoder decoder = new();
307+
TestQPackHeaderHandler testHandler = new();
308+
var success = decoder.DecodeHeader(new ReadOnlySequence<byte>(data), testHandler, out long consumed);
309+
Assert.True(success);
310+
Assert.Single(testHandler.Headers);
311+
Assert.Equal("a", testHandler.Headers[headerName]);
312+
}
313+
314+
public static IEnumerable<TheoryDataRow<string, byte>> KnownAspNetHeaders =>
315+
[
316+
new(HeaderNames.Age, 2),
317+
new(HeaderNames.ContentDisposition, 3),
318+
new(HeaderNames.ContentLength, 4),
319+
new(HeaderNames.Cookie, 5),
320+
new(HeaderNames.Date, 6),
321+
new(HeaderNames.ETag, 7),
322+
new(HeaderNames.IfModifiedSince, 8),
323+
new(HeaderNames.IfNoneMatch, 9),
324+
new(HeaderNames.LastModified, 10),
325+
new(HeaderNames.Link, 11),
326+
new(HeaderNames.Location, 12),
327+
new(HeaderNames.Referer, 13),
328+
new(HeaderNames.SetCookie, 14),
329+
new(HeaderNames.Accept, 29),
330+
new(HeaderNames.Accept, 30),
331+
new(HeaderNames.AcceptEncoding, 31),
332+
new(HeaderNames.AcceptRanges, 32),
333+
new(HeaderNames.AccessControlAllowHeaders, 33),
334+
new(HeaderNames.AccessControlAllowHeaders, 34),
335+
new(HeaderNames.AccessControlAllowOrigin, 35),
336+
new(HeaderNames.CacheControl, 36),
337+
new(HeaderNames.CacheControl, 37),
338+
new(HeaderNames.CacheControl, 38),
339+
new(HeaderNames.CacheControl, 39),
340+
new(HeaderNames.CacheControl, 40),
341+
new(HeaderNames.CacheControl, 41),
342+
new(HeaderNames.ContentEncoding, 42),
343+
new(HeaderNames.ContentEncoding, 43),
344+
new(HeaderNames.ContentType, 44),
345+
new(HeaderNames.ContentType, 45),
346+
new(HeaderNames.ContentType, 46),
347+
new(HeaderNames.ContentType, 47),
348+
new(HeaderNames.ContentType, 48),
349+
new(HeaderNames.ContentType, 49),
350+
new(HeaderNames.ContentType, 50),
351+
new(HeaderNames.ContentType, 51),
352+
new(HeaderNames.ContentType, 52),
353+
new(HeaderNames.ContentType, 53),
354+
new(HeaderNames.ContentType, 54),
355+
new(HeaderNames.Range, 55),
356+
new(HeaderNames.StrictTransportSecurity, 56),
357+
new(HeaderNames.StrictTransportSecurity, 57),
358+
new(HeaderNames.StrictTransportSecurity, 58),
359+
new(HeaderNames.Vary, 59),
360+
new(HeaderNames.Vary, 60),
361+
new(HeaderNames.XContentTypeOptions, 61),
362+
new(HeaderNames.XXSSProtection, 62),
363+
new(HeaderNames.AcceptLanguage, 72),
364+
new(HeaderNames.AccessControlAllowCredentials, 73),
365+
new(HeaderNames.AccessControlAllowCredentials, 74),
366+
new(HeaderNames.AccessControlAllowHeaders, 75),
367+
new(HeaderNames.AccessControlAllowMethods, 76),
368+
new(HeaderNames.AccessControlAllowMethods, 77),
369+
new(HeaderNames.AccessControlAllowMethods, 78),
370+
new(HeaderNames.AccessControlExposeHeaders, 79),
371+
new(HeaderNames.AccessControlRequestHeaders, 80),
372+
new(HeaderNames.AccessControlRequestMethod, 81),
373+
new(HeaderNames.AccessControlRequestMethod, 82),
374+
new(HeaderNames.AltSvc, 83),
375+
new(HeaderNames.Authorization, 84),
376+
new(HeaderNames.ContentSecurityPolicy, 85),
377+
new(HeaderNames.IfRange, 89),
378+
new(HeaderNames.Origin, 90),
379+
new(HeaderNames.Server, 92),
380+
new(HeaderNames.UpgradeInsecureRequests, 94),
381+
new(HeaderNames.UserAgent, 95),
382+
new(HeaderNames.XFrameOptions, 97),
383+
new(HeaderNames.XFrameOptions, 98),
384+
];
385+
386+
278387
private class TestQPackHeaderHandler : IQPackHeaderHandler
279388
{
280389
internal Dictionary<string, string> Headers { get; } = new();

0 commit comments

Comments
 (0)