diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 5ec3b164b67e..0266660792fa 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -34,6 +34,8 @@ internal abstract partial class HttpProtocol : IHttpResponseControl private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive"); private static readonly byte[] _bytesTransferEncodingChunked = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: chunked"); private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: " + Constants.ServerName); + internal const string SchemeHttp = "http"; + internal const string SchemeHttps = "https"; protected BodyControl _bodyControl; private Stack, object>> _onStarting; @@ -385,7 +387,7 @@ public void Reset() if (_scheme == null) { var tlsFeature = ConnectionFeatures?[typeof(ITlsConnectionFeature)]; - _scheme = tlsFeature != null ? "https" : "http"; + _scheme = tlsFeature != null ? SchemeHttps : SchemeHttp; } Scheme = _scheme; @@ -518,7 +520,7 @@ public virtual void OnHeader(ReadOnlySpan name, ReadOnlySpan value) HttpRequestHeaders.Append(name, value); } - public virtual void OnHeader(int index, ReadOnlySpan name, ReadOnlySpan value) + public virtual void OnHeader(int index, bool indexOnly, ReadOnlySpan name, ReadOnlySpan value) { IncrementRequestHeadersCount(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs index c914595bc9c9..bcb67975e5eb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs @@ -151,13 +151,15 @@ private static HeaderEncodingHint ResolveHeaderEncodingHint(int staticTableId, s private static bool IsSensitive(int staticTableIndex, string name) { // Set-Cookie could contain sensitive data. - if (staticTableIndex == H2StaticTable.SetCookie) + switch (staticTableIndex) { - return true; - } - if (string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase)) - { - return true; + case H2StaticTable.SetCookie: + case H2StaticTable.ContentDisposition: + return true; + case -1: + // Content-Disposition currently isn't a known header so a + // static index probably won't be specified. + return string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase); } return false; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 3040c54107af..b63f4a4e7cb2 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -1226,12 +1226,9 @@ private void UpdateConnectionState() } } - // We can't throw a Http2StreamErrorException here, it interrupts the header decompression state and may corrupt subsequent header frames on other streams. - // 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 - // rework the flow so that the remaining headers are drained and the decompression state is maintained. public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) { - OnHeaderCore(index: null, name, value); + OnHeaderCore(index: null, indexedValue: false, name, value); } public void OnStaticIndexedHeader(int index) @@ -1239,20 +1236,20 @@ public void OnStaticIndexedHeader(int index) Debug.Assert(index <= H2StaticTable.Count); ref readonly var entry = ref H2StaticTable.Get(index - 1); - OnHeaderCore(index, entry.Name, entry.Value); + OnHeaderCore(index, indexedValue: true, entry.Name, entry.Value); } public void OnStaticIndexedHeader(int index, ReadOnlySpan value) { Debug.Assert(index <= H2StaticTable.Count); - OnHeaderCore(index, H2StaticTable.Get(index - 1).Name, value); + OnHeaderCore(index, indexedValue: false, H2StaticTable.Get(index - 1).Name, value); } // We can't throw a Http2StreamErrorException here, it interrupts the header decompression state and may corrupt subsequent header frames on other streams. // 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 // rework the flow so that the remaining headers are drained and the decompression state is maintained. - private void OnHeaderCore(int? index, ReadOnlySpan name, ReadOnlySpan value) + private void OnHeaderCore(int? index, bool indexedValue, ReadOnlySpan name, ReadOnlySpan value) { // https://tools.ietf.org/html/rfc7540#section-6.5.2 // "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 name, ReadOnlySpan name, ReadOnlySpan value) + public override void OnHeader(int index, bool indexedValue, ReadOnlySpan name, ReadOnlySpan value) { - base.OnHeader(index, name, value); + base.OnHeader(index, indexedValue, name, value); + + if (indexedValue) + { + // Special case setting headers when the value is indexed for performance. + switch (index) + { + case H2StaticTable.MethodGet: + HttpRequestHeaders.HeaderMethod = HttpMethods.Get; + Method = HttpMethod.Get; + _methodText = HttpMethods.Get; + return; + case H2StaticTable.MethodPost: + HttpRequestHeaders.HeaderMethod = HttpMethods.Post; + Method = HttpMethod.Post; + _methodText = HttpMethods.Post; + return; + case H2StaticTable.SchemeHttp: + HttpRequestHeaders.HeaderScheme = SchemeHttp; + return; + case H2StaticTable.SchemeHttps: + HttpRequestHeaders.HeaderScheme = SchemeHttps; + return; + } + } // HPack append will return false if the index is not a known request header. // For example, someone could send the index of "Server" (a response header) in the request. diff --git a/src/Servers/Kestrel/Core/src/ListenOptions.cs b/src/Servers/Kestrel/Core/src/ListenOptions.cs index bc90bb2fbddd..fb049d102afd 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptions.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; namespace Microsoft.AspNetCore.Server.Kestrel.Core { @@ -84,7 +85,7 @@ internal string Scheme { get { - return IsTls ? "https" : "http"; + return IsTls ? HttpProtocol.SchemeHttps : HttpProtocol.SchemeHttp; } } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/HPackHeaderWriterBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/HPackHeaderWriterBenchmark.cs new file mode 100644 index 000000000000..fcca5b58aa1b --- /dev/null +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/HPackHeaderWriterBenchmark.cs @@ -0,0 +1,68 @@ +// 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; +using System.Linq; +using System.Net.Http.HPack; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class HPackHeaderWriterBenchmark + { + private Http2HeadersEnumerator _http2HeadersEnumerator; + private HPackEncoder _hpackEncoder; + private HttpResponseHeaders _knownResponseHeaders; + private HttpResponseHeaders _unknownResponseHeaders; + private byte[] _buffer; + + [GlobalSetup] + public void GlobalSetup() + { + _http2HeadersEnumerator = new Http2HeadersEnumerator(); + _hpackEncoder = new HPackEncoder(); + _buffer = new byte[1024 * 1024]; + + _knownResponseHeaders = new HttpResponseHeaders + { + HeaderServer = "Kestrel", + HeaderContentType = "application/json", + HeaderDate = "Date!", + HeaderContentLength = "0", + HeaderAcceptRanges = "Ranges!", + HeaderTransferEncoding = "Encoding!", + HeaderVia = "Via!", + HeaderVary = "Vary!", + HeaderWWWAuthenticate = "Authenticate!", + HeaderLastModified = "Modified!", + HeaderExpires = "Expires!", + HeaderAge = "Age!" + }; + + _unknownResponseHeaders = new HttpResponseHeaders(); + for (var i = 0; i < 10; i++) + { + _unknownResponseHeaders.Append("Unknown" + i, "Value" + i); + } + } + + [Benchmark] + public void BeginEncodeHeaders_KnownHeaders() + { + _http2HeadersEnumerator.Initialize(_knownResponseHeaders); + HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _); + } + + [Benchmark] + public void BeginEncodeHeaders_UnknownHeaders() + { + _http2HeadersEnumerator.Initialize(_unknownResponseHeaders); + HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _); + } + } +}