Skip to content

Commit 58a7592

Browse files
authored
HTTP2: Optimize header processing (#24945)
1 parent bbb851e commit 58a7592

File tree

6 files changed

+120
-21
lines changed

6 files changed

+120
-21
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ internal abstract partial class HttpProtocol : IHttpResponseControl
3434
private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive");
3535
private static readonly byte[] _bytesTransferEncodingChunked = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: chunked");
3636
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: " + Constants.ServerName);
37+
internal const string SchemeHttp = "http";
38+
internal const string SchemeHttps = "https";
3739

3840
protected BodyControl _bodyControl;
3941
private Stack<KeyValuePair<Func<object, Task>, object>> _onStarting;
@@ -385,7 +387,7 @@ public void Reset()
385387
if (_scheme == null)
386388
{
387389
var tlsFeature = ConnectionFeatures?[typeof(ITlsConnectionFeature)];
388-
_scheme = tlsFeature != null ? "https" : "http";
390+
_scheme = tlsFeature != null ? SchemeHttps : SchemeHttp;
389391
}
390392

391393
Scheme = _scheme;
@@ -518,7 +520,7 @@ public virtual void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
518520
HttpRequestHeaders.Append(name, value);
519521
}
520522

521-
public virtual void OnHeader(int index, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
523+
public virtual void OnHeader(int index, bool indexOnly, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
522524
{
523525
IncrementRequestHeadersCount();
524526

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,15 @@ private static HeaderEncodingHint ResolveHeaderEncodingHint(int staticTableId, s
151151
private static bool IsSensitive(int staticTableIndex, string name)
152152
{
153153
// Set-Cookie could contain sensitive data.
154-
if (staticTableIndex == H2StaticTable.SetCookie)
154+
switch (staticTableIndex)
155155
{
156-
return true;
157-
}
158-
if (string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase))
159-
{
160-
return true;
156+
case H2StaticTable.SetCookie:
157+
case H2StaticTable.ContentDisposition:
158+
return true;
159+
case -1:
160+
// Content-Disposition currently isn't a known header so a
161+
// static index probably won't be specified.
162+
return string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase);
161163
}
162164

163165
return false;

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,33 +1226,30 @@ private void UpdateConnectionState()
12261226
}
12271227
}
12281228

1229-
// We can't throw a Http2StreamErrorException here, it interrupts the header decompression state and may corrupt subsequent header frames on other streams.
1230-
// For now these either need to be connection errors or BadRequests. If we want to downgrade any of them to stream errors later then we need to
1231-
// rework the flow so that the remaining headers are drained and the decompression state is maintained.
12321229
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
12331230
{
1234-
OnHeaderCore(index: null, name, value);
1231+
OnHeaderCore(index: null, indexedValue: false, name, value);
12351232
}
12361233

12371234
public void OnStaticIndexedHeader(int index)
12381235
{
12391236
Debug.Assert(index <= H2StaticTable.Count);
12401237

12411238
ref readonly var entry = ref H2StaticTable.Get(index - 1);
1242-
OnHeaderCore(index, entry.Name, entry.Value);
1239+
OnHeaderCore(index, indexedValue: true, entry.Name, entry.Value);
12431240
}
12441241

12451242
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
12461243
{
12471244
Debug.Assert(index <= H2StaticTable.Count);
12481245

1249-
OnHeaderCore(index, H2StaticTable.Get(index - 1).Name, value);
1246+
OnHeaderCore(index, indexedValue: false, H2StaticTable.Get(index - 1).Name, value);
12501247
}
12511248

12521249
// We can't throw a Http2StreamErrorException here, it interrupts the header decompression state and may corrupt subsequent header frames on other streams.
12531250
// For now these either need to be connection errors or BadRequests. If we want to downgrade any of them to stream errors later then we need to
12541251
// rework the flow so that the remaining headers are drained and the decompression state is maintained.
1255-
private void OnHeaderCore(int? index, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
1252+
private void OnHeaderCore(int? index, bool indexedValue, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
12561253
{
12571254
// https://tools.ietf.org/html/rfc7540#section-6.5.2
12581255
// "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field.";
@@ -1283,7 +1280,7 @@ private void OnHeaderCore(int? index, ReadOnlySpan<byte> name, ReadOnlySpan<byte
12831280
// Throws InvalidOperation for bad encoding.
12841281
if (index != null)
12851282
{
1286-
_currentHeadersStream.OnHeader(index.Value, name, value);
1283+
_currentHeadersStream.OnHeader(index.Value, indexedValue, name, value);
12871284
}
12881285
else
12891286
{

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

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.IO;
88
using System.IO.Pipelines;
9+
using System.Net.Http.HPack;
910
using System.Runtime.CompilerServices;
1011
using System.Threading;
1112
using System.Threading.Tasks;
@@ -15,6 +16,7 @@
1516
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
1617
using Microsoft.Extensions.Primitives;
1718
using Microsoft.Net.Http.Headers;
19+
using HttpMethods = Microsoft.AspNetCore.Http.HttpMethods;
1820

1921
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
2022
{
@@ -205,7 +207,8 @@ private bool TryValidatePseudoHeaders()
205207

206208
_httpVersion = Http.HttpVersion.Http2;
207209

208-
if (!TryValidateMethod())
210+
// Method could already have been set from :method static table index
211+
if (Method == HttpMethod.None && !TryValidateMethod())
209212
{
210213
return false;
211214
}
@@ -237,7 +240,9 @@ private bool TryValidatePseudoHeaders()
237240
// - That said, we shouldn't allow arbitrary values or use them to populate Request.Scheme, right?
238241
// - For now we'll restrict it to http/s and require it match the transport.
239242
// - We'll need to find some concrete scenarios to warrant unblocking this.
240-
if (!string.Equals(HttpRequestHeaders.HeaderScheme, Scheme, StringComparison.OrdinalIgnoreCase))
243+
var headerScheme = HttpRequestHeaders.HeaderScheme.ToString();
244+
if (!ReferenceEquals(headerScheme, Scheme) &&
245+
!string.Equals(headerScheme, Scheme, StringComparison.OrdinalIgnoreCase))
241246
{
242247
ResetAndAbort(new ConnectionAbortedException(
243248
CoreStrings.FormatHttp2StreamErrorSchemeMismatch(HttpRequestHeaders.HeaderScheme, Scheme)), Http2ErrorCode.PROTOCOL_ERROR);
@@ -620,9 +625,33 @@ private enum StreamCompletionFlags
620625
Aborted = 4,
621626
}
622627

623-
public override void OnHeader(int index, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
628+
public override void OnHeader(int index, bool indexedValue, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
624629
{
625-
base.OnHeader(index, name, value);
630+
base.OnHeader(index, indexedValue, name, value);
631+
632+
if (indexedValue)
633+
{
634+
// Special case setting headers when the value is indexed for performance.
635+
switch (index)
636+
{
637+
case H2StaticTable.MethodGet:
638+
HttpRequestHeaders.HeaderMethod = HttpMethods.Get;
639+
Method = HttpMethod.Get;
640+
_methodText = HttpMethods.Get;
641+
return;
642+
case H2StaticTable.MethodPost:
643+
HttpRequestHeaders.HeaderMethod = HttpMethods.Post;
644+
Method = HttpMethod.Post;
645+
_methodText = HttpMethods.Post;
646+
return;
647+
case H2StaticTable.SchemeHttp:
648+
HttpRequestHeaders.HeaderScheme = SchemeHttp;
649+
return;
650+
case H2StaticTable.SchemeHttps:
651+
HttpRequestHeaders.HeaderScheme = SchemeHttps;
652+
return;
653+
}
654+
}
626655

627656
// HPack append will return false if the index is not a known request header.
628657
// For example, someone could send the index of "Server" (a response header) in the request.

src/Servers/Kestrel/Core/src/ListenOptions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Threading.Tasks;
99
using Microsoft.AspNetCore.Connections;
1010
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
11+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
1112

1213
namespace Microsoft.AspNetCore.Server.Kestrel.Core
1314
{
@@ -84,7 +85,7 @@ internal string Scheme
8485
{
8586
get
8687
{
87-
return IsTls ? "https" : "http";
88+
return IsTls ? HttpProtocol.SchemeHttps : HttpProtocol.SchemeHttp;
8889
}
8990
}
9091

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Linq;
6+
using System.Net.Http.HPack;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using BenchmarkDotNet.Attributes;
10+
using Microsoft.AspNetCore.Http;
11+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
12+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
13+
14+
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
15+
{
16+
public class HPackHeaderWriterBenchmark
17+
{
18+
private Http2HeadersEnumerator _http2HeadersEnumerator;
19+
private HPackEncoder _hpackEncoder;
20+
private HttpResponseHeaders _knownResponseHeaders;
21+
private HttpResponseHeaders _unknownResponseHeaders;
22+
private byte[] _buffer;
23+
24+
[GlobalSetup]
25+
public void GlobalSetup()
26+
{
27+
_http2HeadersEnumerator = new Http2HeadersEnumerator();
28+
_hpackEncoder = new HPackEncoder();
29+
_buffer = new byte[1024 * 1024];
30+
31+
_knownResponseHeaders = new HttpResponseHeaders
32+
{
33+
HeaderServer = "Kestrel",
34+
HeaderContentType = "application/json",
35+
HeaderDate = "Date!",
36+
HeaderContentLength = "0",
37+
HeaderAcceptRanges = "Ranges!",
38+
HeaderTransferEncoding = "Encoding!",
39+
HeaderVia = "Via!",
40+
HeaderVary = "Vary!",
41+
HeaderWWWAuthenticate = "Authenticate!",
42+
HeaderLastModified = "Modified!",
43+
HeaderExpires = "Expires!",
44+
HeaderAge = "Age!"
45+
};
46+
47+
_unknownResponseHeaders = new HttpResponseHeaders();
48+
for (var i = 0; i < 10; i++)
49+
{
50+
_unknownResponseHeaders.Append("Unknown" + i, "Value" + i);
51+
}
52+
}
53+
54+
[Benchmark]
55+
public void BeginEncodeHeaders_KnownHeaders()
56+
{
57+
_http2HeadersEnumerator.Initialize(_knownResponseHeaders);
58+
HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _);
59+
}
60+
61+
[Benchmark]
62+
public void BeginEncodeHeaders_UnknownHeaders()
63+
{
64+
_http2HeadersEnumerator.Initialize(_unknownResponseHeaders);
65+
HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _);
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)