Skip to content

Commit 5adbdcb

Browse files
authored
HTTP/3: Improve static table compression to include values (#38681)
1 parent 3b6ad60 commit 5adbdcb

File tree

7 files changed

+449
-218
lines changed

7 files changed

+449
-218
lines changed

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

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,179 @@ internal enum KnownHeaderType
102102
WWWAuthenticate,
103103
}
104104

105+
internal static class HttpHeadersCompression
106+
{
107+
internal static (int index, bool matchedValue) MatchKnownHeaderQPack(KnownHeaderType knownHeader, string value)
108+
{
109+
switch (knownHeader)
110+
{
111+
case KnownHeaderType.Age:
112+
switch (value)
113+
{
114+
case "0":
115+
return (2, true);
116+
default:
117+
return (2, false);
118+
}
119+
case KnownHeaderType.ContentLength:
120+
switch (value)
121+
{
122+
case "0":
123+
return (4, true);
124+
default:
125+
return (4, false);
126+
}
127+
case KnownHeaderType.Date:
128+
return (6, false);
129+
case KnownHeaderType.ETag:
130+
return (7, false);
131+
case KnownHeaderType.LastModified:
132+
return (10, false);
133+
case KnownHeaderType.Location:
134+
return (12, false);
135+
case KnownHeaderType.SetCookie:
136+
return (14, false);
137+
case KnownHeaderType.AcceptRanges:
138+
switch (value)
139+
{
140+
case "bytes":
141+
return (32, true);
142+
default:
143+
return (32, false);
144+
}
145+
case KnownHeaderType.AccessControlAllowHeaders:
146+
switch (value)
147+
{
148+
case "cache-control":
149+
return (33, true);
150+
case "content-type":
151+
return (34, true);
152+
case "*":
153+
return (75, true);
154+
default:
155+
return (33, false);
156+
}
157+
case KnownHeaderType.AccessControlAllowOrigin:
158+
switch (value)
159+
{
160+
case "*":
161+
return (35, true);
162+
default:
163+
return (35, false);
164+
}
165+
case KnownHeaderType.CacheControl:
166+
switch (value)
167+
{
168+
case "max-age=0":
169+
return (36, true);
170+
case "max-age=2592000":
171+
return (37, true);
172+
case "max-age=604800":
173+
return (38, true);
174+
case "no-cache":
175+
return (39, true);
176+
case "no-store":
177+
return (40, true);
178+
case "public, max-age=31536000":
179+
return (41, true);
180+
default:
181+
return (36, false);
182+
}
183+
case KnownHeaderType.ContentEncoding:
184+
switch (value)
185+
{
186+
case "br":
187+
return (42, true);
188+
case "gzip":
189+
return (43, true);
190+
default:
191+
return (42, false);
192+
}
193+
case KnownHeaderType.ContentType:
194+
switch (value)
195+
{
196+
case "application/dns-message":
197+
return (44, true);
198+
case "application/javascript":
199+
return (45, true);
200+
case "application/json":
201+
return (46, true);
202+
case "application/x-www-form-urlencoded":
203+
return (47, true);
204+
case "image/gif":
205+
return (48, true);
206+
case "image/jpeg":
207+
return (49, true);
208+
case "image/png":
209+
return (50, true);
210+
case "text/css":
211+
return (51, true);
212+
case "text/html; charset=utf-8":
213+
return (52, true);
214+
case "text/plain":
215+
return (53, true);
216+
case "text/plain;charset=utf-8":
217+
return (54, true);
218+
default:
219+
return (44, false);
220+
}
221+
case KnownHeaderType.Vary:
222+
switch (value)
223+
{
224+
case "accept-encoding":
225+
return (59, true);
226+
case "origin":
227+
return (60, true);
228+
default:
229+
return (59, false);
230+
}
231+
case KnownHeaderType.AccessControlAllowCredentials:
232+
switch (value)
233+
{
234+
case "FALSE":
235+
return (73, true);
236+
case "TRUE":
237+
return (74, true);
238+
default:
239+
return (73, false);
240+
}
241+
case KnownHeaderType.AccessControlAllowMethods:
242+
switch (value)
243+
{
244+
case "get":
245+
return (76, true);
246+
case "get, post, options":
247+
return (77, true);
248+
case "options":
249+
return (78, true);
250+
default:
251+
return (76, false);
252+
}
253+
case KnownHeaderType.AccessControlExposeHeaders:
254+
switch (value)
255+
{
256+
case "content-length":
257+
return (79, true);
258+
default:
259+
return (79, false);
260+
}
261+
case KnownHeaderType.AltSvc:
262+
switch (value)
263+
{
264+
case "clear":
265+
return (83, true);
266+
default:
267+
return (83, false);
268+
}
269+
case KnownHeaderType.Server:
270+
return (92, false);
271+
272+
default:
273+
return (-1, false);
274+
}
275+
}
276+
}
277+
105278
internal partial class HttpHeaders
106279
{
107280
private readonly static HashSet<string> _internedHeaderNames = new HashSet<string>(96, StringComparer.OrdinalIgnoreCase)

src/Servers/Kestrel/Core/src/Internal/Http3/Http3HeadersEnumerator.cs

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ private enum HeadersType : byte
2929

3030
public Func<string, Encoding?> EncodingSelector { get; set; } = KestrelServerOptions.DefaultHeaderEncodingSelector;
3131

32-
public int QPackStaticTableId => GetResponseHeaderStaticTableId(_knownHeaderType);
32+
public (int index, bool matchedValue) GetQPackStaticTableId() => HttpHeadersCompression.MatchKnownHeaderQPack(_knownHeaderType, Current.Value);
3333
public KeyValuePair<string, string> Current { get; private set; }
3434
object IEnumerator.Current => Current;
3535

@@ -145,66 +145,4 @@ public void Reset()
145145
public void Dispose()
146146
{
147147
}
148-
149-
internal static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType)
150-
{
151-
// Removed from this test are request-only headers, e.g. cookie.
152-
//
153-
// Not every header in the QPACK static table is known.
154-
// These are missing from this test and the full header name is written.
155-
// Missing:
156-
// - link
157-
// - location
158-
// - strict-transport-security
159-
// - x-content-type-options
160-
// - x-xss-protection
161-
// - content-security-policy
162-
// - early-data
163-
// - expect-ct
164-
// - purpose
165-
// - timing-allow-origin
166-
// - x-forwarded-for
167-
// - x-frame-options
168-
switch (responseHeaderType)
169-
{
170-
case KnownHeaderType.Age:
171-
return H3StaticTable.Age0;
172-
case KnownHeaderType.ContentLength:
173-
return H3StaticTable.ContentLength0;
174-
case KnownHeaderType.Date:
175-
return H3StaticTable.Date;
176-
case KnownHeaderType.ETag:
177-
return H3StaticTable.ETag;
178-
case KnownHeaderType.LastModified:
179-
return H3StaticTable.LastModified;
180-
case KnownHeaderType.Location:
181-
return H3StaticTable.Location;
182-
case KnownHeaderType.SetCookie:
183-
return H3StaticTable.SetCookie;
184-
case KnownHeaderType.AcceptRanges:
185-
return H3StaticTable.AcceptRangesBytes;
186-
case KnownHeaderType.AccessControlAllowHeaders:
187-
return H3StaticTable.AccessControlAllowHeadersCacheControl;
188-
case KnownHeaderType.AccessControlAllowOrigin:
189-
return H3StaticTable.AccessControlAllowOriginAny;
190-
case KnownHeaderType.CacheControl:
191-
return H3StaticTable.CacheControlMaxAge0;
192-
case KnownHeaderType.ContentEncoding:
193-
return H3StaticTable.ContentEncodingBr;
194-
case KnownHeaderType.ContentType:
195-
return H3StaticTable.ContentTypeApplicationDnsMessage;
196-
case KnownHeaderType.Vary:
197-
return H3StaticTable.VaryAcceptEncoding;
198-
case KnownHeaderType.AccessControlAllowCredentials:
199-
return H3StaticTable.AccessControlAllowCredentials;
200-
case KnownHeaderType.AccessControlAllowMethods:
201-
return H3StaticTable.AccessControlAllowMethodsGet;
202-
case KnownHeaderType.AltSvc:
203-
return H3StaticTable.AltSvcClear;
204-
case KnownHeaderType.Server:
205-
return H3StaticTable.Server;
206-
default:
207-
return -1;
208-
}
209-
}
210148
}

src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,39 @@ private static bool Encode(Http3HeadersEnumerator headersEnumerator, Span<byte>
5959

6060
do
6161
{
62-
var staticTableId = headersEnumerator.QPackStaticTableId;
62+
// Match the current header to the QPACK static table. Possible outcomes:
63+
// 1. Known header and value. Write index.
64+
// 2. Known header with custom value. Write name index and full value.
65+
// 3. Unknown header. Write full name and value.
66+
var (staticTableId, matchedValue) = headersEnumerator.GetQPackStaticTableId();
6367
var name = headersEnumerator.Current.Key;
6468
var value = headersEnumerator.Current.Value;
65-
var valueEncoding = ReferenceEquals(headersEnumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector)
66-
? null : headersEnumerator.EncodingSelector(name);
6769

68-
if (!EncodeHeader(buffer.Slice(length), staticTableId, name, value, valueEncoding, out var headerLength))
70+
int headerLength;
71+
if (matchedValue)
6972
{
70-
if (length == 0 && throwIfNoneEncoded)
73+
if (!QPackEncoder.EncodeStaticIndexedHeaderField(staticTableId, buffer.Slice(length), out headerLength))
7174
{
72-
throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */);
75+
if (length == 0 && throwIfNoneEncoded)
76+
{
77+
throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */);
78+
}
79+
return false;
80+
}
81+
}
82+
else
83+
{
84+
var valueEncoding = ReferenceEquals(headersEnumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector)
85+
? null : headersEnumerator.EncodingSelector(name);
86+
87+
if (!EncodeHeader(buffer.Slice(length), staticTableId, name, value, valueEncoding, out headerLength))
88+
{
89+
if (length == 0 && throwIfNoneEncoded)
90+
{
91+
throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */);
92+
}
93+
return false;
7394
}
74-
return false;
7595
}
7696

7797
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.3

src/Servers/Kestrel/Core/test/Http3/Http3HeadersEnumeratorTests.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,20 @@ public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource()
9393
Assert.True(e.MoveNext());
9494
Assert.Equal("Name1", e.Current.Key);
9595
Assert.Equal("Value1", e.Current.Value);
96-
Assert.Equal(-1, e.QPackStaticTableId);
96+
var (index, matchedValue) = e.GetQPackStaticTableId();
97+
Assert.Equal(-1, index);
9798

9899
Assert.True(e.MoveNext());
99100
Assert.Equal("Name2", e.Current.Key);
100101
Assert.Equal("Value2-1", e.Current.Value);
101-
Assert.Equal(-1, e.QPackStaticTableId);
102+
(index, matchedValue) = e.GetQPackStaticTableId();
103+
Assert.Equal(-1, index);
102104

103105
Assert.True(e.MoveNext());
104106
Assert.Equal("Name2", e.Current.Key);
105107
Assert.Equal("Value2-2", e.Current.Value);
106-
Assert.Equal(-1, e.QPackStaticTableId);
108+
(index, matchedValue) = e.GetQPackStaticTableId();
109+
Assert.Equal(-1, index);
107110

108111
var responseTrailers = (IHeaderDictionary)new HttpResponseTrailers();
109112

@@ -118,22 +121,26 @@ public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource()
118121
Assert.True(e.MoveNext());
119122
Assert.Equal("Grpc-Status", e.Current.Key);
120123
Assert.Equal("1", e.Current.Value);
121-
Assert.Equal(-1, e.QPackStaticTableId);
124+
(index, matchedValue) = e.GetQPackStaticTableId();
125+
Assert.Equal(-1, index);
122126

123127
Assert.True(e.MoveNext());
124128
Assert.Equal("Name1", e.Current.Key);
125129
Assert.Equal("Value1", e.Current.Value);
126-
Assert.Equal(-1, e.QPackStaticTableId);
130+
(index, matchedValue) = e.GetQPackStaticTableId();
131+
Assert.Equal(-1, index);
127132

128133
Assert.True(e.MoveNext());
129134
Assert.Equal("Name2", e.Current.Key);
130135
Assert.Equal("Value2-1", e.Current.Value);
131-
Assert.Equal(-1, e.QPackStaticTableId);
136+
(index, matchedValue) = e.GetQPackStaticTableId();
137+
Assert.Equal(-1, index);
132138

133139
Assert.True(e.MoveNext());
134140
Assert.Equal("Name2", e.Current.Key);
135141
Assert.Equal("Value2-2", e.Current.Value);
136-
Assert.Equal(-1, e.QPackStaticTableId);
142+
(index, matchedValue) = e.GetQPackStaticTableId();
143+
Assert.Equal(-1, index);
137144

138145
Assert.False(e.MoveNext());
139146
}
@@ -143,7 +150,7 @@ public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource()
143150
var headers = new List<(int HPackStaticTableId, string Name, string Value)>();
144151
while (enumerator.MoveNext())
145152
{
146-
headers.Add(CreateHeaderResult(enumerator.QPackStaticTableId, enumerator.Current.Key, enumerator.Current.Value));
153+
headers.Add(CreateHeaderResult(enumerator.GetQPackStaticTableId().index, enumerator.Current.Key, enumerator.Current.Value));
147154
}
148155
return headers.ToArray();
149156
}

0 commit comments

Comments
 (0)