From b446ab7c6f28bb10879bd355afe08bbf283c7de3 Mon Sep 17 00:00:00 2001
From: Stephen Halter <halter73@gmail.com>
Date: Fri, 26 Jun 2020 19:49:35 -0700
Subject: [PATCH] Add custom request header decoder API to Kestrel (#23233)

---
 ...pNetCore.Server.Kestrel.Core.netcoreapp.cs |  1 +
 .../Core/src/Internal/ConfigurationReader.cs  |  3 -
 .../Internal/Http/HttpHeaders.Generated.cs    | 70 +++++++++++++++++-
 .../Core/src/Internal/Http/HttpProtocol.cs    |  4 +-
 .../src/Internal/Http/HttpRequestHeaders.cs   | 40 ++++++++--
 .../Internal/Infrastructure/HttpUtilities.cs  | 37 ++++++++--
 .../Core/src/KestrelConfigurationLoader.cs    |  2 -
 src/Servers/Kestrel/Core/src/KestrelServer.cs | 16 +++-
 .../Kestrel/Core/src/KestrelServerOptions.cs  | 40 ++++++----
 .../Core/test/HttpRequestHeadersTests.cs      | 74 ++++++++++++++++---
 .../Core/test/KestrelServerOptionsTests.cs    | 10 +++
 src/Servers/Kestrel/Core/test/UTF8Decoding.cs |  6 +-
 .../test/KestrelConfigurationLoaderTests.cs   | 22 +-----
 .../BytesToStringBenchmark.cs                 |  9 ++-
 src/Servers/Kestrel/shared/KnownHeaders.cs    | 45 +++++++----
 .../Http2/Http2StreamTests.cs                 |  2 +-
 .../Http2/Http2TestBase.cs                    |  3 +-
 .../Http3/Http3TestBase.cs                    |  2 +-
 .../InMemory.FunctionalTests/RequestTests.cs  | 38 +++++++++-
 .../ServerInfrastructure/StringUtilities.cs   | 12 +--
 20 files changed, 336 insertions(+), 100 deletions(-)

diff --git a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs
index 31565455670..c59df1e7ac3 100644
--- a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs
+++ b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs
@@ -137,6 +137,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
         public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
         public bool EnableAltSvc { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
         public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
+        public System.Func<string, System.Text.Encoding> RequestHeaderEncodingSelector { get { throw null; } set { } }
         public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; }
         public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; }
         public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config, bool reloadOnChange) { throw null; }
diff --git a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs b/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
index 19ec02774a6..40ac8cd9ffc 100644
--- a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
@@ -18,14 +18,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
         private const string EndpointDefaultsKey = "EndpointDefaults";
         private const string EndpointsKey = "Endpoints";
         private const string UrlKey = "Url";
-        private const string Latin1RequestHeadersKey = "Latin1RequestHeaders";
 
         private readonly IConfiguration _configuration;
 
         private IDictionary<string, CertificateConfig> _certificates;
         private EndpointDefaults _endpointDefaults;
         private IEnumerable<EndpointConfig> _endpoints;
-        private bool? _latin1RequestHeaders;
 
         public ConfigurationReader(IConfiguration configuration)
         {
@@ -35,7 +33,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
         public IDictionary<string, CertificateConfig> Certificates => _certificates ??= ReadCertificates();
         public EndpointDefaults EndpointDefaults => _endpointDefaults ??= ReadEndpointDefaults();
         public IEnumerable<EndpointConfig> Endpoints => _endpoints ??= ReadEndpoints();
-        public bool Latin1RequestHeaders => _latin1RequestHeaders ??= _configuration.GetValue<bool>(Latin1RequestHeadersKey);
 
         private IDictionary<string, CertificateConfig> ReadCertificates()
         {
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs
index 516d7c32ecc..03886c20400 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs
@@ -6270,6 +6270,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
         public unsafe void Append(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
         {
             ref byte nameStart = ref MemoryMarshal.GetReference(name);
+            var nameStr = string.Empty;
             ref StringValues values = ref Unsafe.AsRef<StringValues>(null);
             var flag = 0L;
 
@@ -6281,6 +6282,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x20000000000L;
                         values = ref _headers._TE;
+                        nameStr = HeaderNames.TE;
                     }
                     break;
                 case 3:
@@ -6289,11 +6291,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x100000000000L;
                         values = ref _headers._DNT;
+                        nameStr = HeaderNames.DNT;
                     }
                     else if ((firstTerm3 == 0x4956u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)2) & 0xdfu) == 0x41u))
                     {
                         flag = 0x100L;
                         values = ref _headers._Via;
+                        nameStr = HeaderNames.Via;
                     }
                     break;
                 case 4:
@@ -6302,16 +6306,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x80000000L;
                         values = ref _headers._Host;
+                        nameStr = HeaderNames.Host;
                     }
                     else if ((firstTerm4 == 0x45544144u))
                     {
                         flag = 0x4L;
                         values = ref _headers._Date;
+                        nameStr = HeaderNames.Date;
                     }
                     else if ((firstTerm4 == 0x4d4f5246u))
                     {
                         flag = 0x40000000L;
                         values = ref _headers._From;
+                        nameStr = HeaderNames.From;
                     }
                     break;
                 case 5:
@@ -6319,16 +6326,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x200000L;
                         values = ref _headers._Path;
+                        nameStr = HeaderNames.Path;
                     }
                     else if (((Unsafe.ReadUnaligned<uint>(ref nameStart) & 0xdfdfdfdfu) == 0x4f4c4c41u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)4) & 0xdfu) == 0x57u))
                     {
                         flag = 0x400L;
                         values = ref _headers._Allow;
+                        nameStr = HeaderNames.Allow;
                     }
                     else if (((Unsafe.ReadUnaligned<uint>(ref nameStart) & 0xdfdfdfdfu) == 0x474e4152u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)4) & 0xdfu) == 0x45u))
                     {
                         flag = 0x10000000000L;
                         values = ref _headers._Range;
+                        nameStr = HeaderNames.Range;
                     }
                     break;
                 case 6:
@@ -6337,26 +6347,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x800000L;
                         values = ref _headers._Accept;
+                        nameStr = HeaderNames.Accept;
                     }
                     else if ((firstTerm6 == 0x4b4f4f43u) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(ushort)))) & 0xdfdfu) == 0x4549u))
                     {
                         flag = 0x10000000L;
                         values = ref _headers._Cookie;
+                        nameStr = HeaderNames.Cookie;
                     }
                     else if ((firstTerm6 == 0x45505845u) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(ushort)))) & 0xdfdfu) == 0x5443u))
                     {
                         flag = 0x20000000L;
                         values = ref _headers._Expect;
+                        nameStr = HeaderNames.Expect;
                     }
                     else if ((firstTerm6 == 0x4749524fu) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(ushort)))) & 0xdfdfu) == 0x4e49u))
                     {
                         flag = 0x4000000000000L;
                         values = ref _headers._Origin;
+                        nameStr = HeaderNames.Origin;
                     }
                     else if ((firstTerm6 == 0x47415250u) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(ushort)))) & 0xdfdfu) == 0x414du))
                     {
                         flag = 0x10L;
                         values = ref _headers._Pragma;
+                        nameStr = HeaderNames.Pragma;
                     }
                     break;
                 case 7:
@@ -6364,36 +6379,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x100000L;
                         values = ref _headers._Method;
+                        nameStr = HeaderNames.Method;
                     }
                     else if (((Unsafe.ReadUnaligned<uint>(ref nameStart) & 0xdfdfdfffu) == 0x4843533au) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(ushort)))) & 0xdfdfu) == 0x4d45u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)6) & 0xdfu) == 0x45u))
                     {
                         flag = 0x400000L;
                         values = ref _headers._Scheme;
+                        nameStr = HeaderNames.Scheme;
                     }
                     else if (((Unsafe.ReadUnaligned<uint>(ref nameStart) & 0xdfdfdfdfu) == 0x49505845u) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(ushort)))) & 0xdfdfu) == 0x4552u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)6) & 0xdfu) == 0x53u))
                     {
                         flag = 0x20000L;
                         values = ref _headers._Expires;
+                        nameStr = HeaderNames.Expires;
                     }
                     else if (((Unsafe.ReadUnaligned<uint>(ref nameStart) & 0xdfdfdfdfu) == 0x45464552u) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(ushort)))) & 0xdfdfu) == 0x4552u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)6) & 0xdfu) == 0x52u))
                     {
                         flag = 0x8000000000L;
                         values = ref _headers._Referer;
+                        nameStr = HeaderNames.Referer;
                     }
                     else if (((Unsafe.ReadUnaligned<uint>(ref nameStart) & 0xdfdfdfdfu) == 0x49415254u) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(ushort)))) & 0xdfdfu) == 0x454cu) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)6) & 0xdfu) == 0x52u))
                     {
                         flag = 0x20L;
                         values = ref _headers._Trailer;
+                        nameStr = HeaderNames.Trailer;
                     }
                     else if (((Unsafe.ReadUnaligned<uint>(ref nameStart) & 0xdfdfdfdfu) == 0x52475055u) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(ushort)))) & 0xdfdfu) == 0x4441u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)6) & 0xdfu) == 0x45u))
                     {
                         flag = 0x80L;
                         values = ref _headers._Upgrade;
+                        nameStr = HeaderNames.Upgrade;
                     }
                     else if (((Unsafe.ReadUnaligned<uint>(ref nameStart) & 0xdfdfdfdfu) == 0x4e524157u) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(ushort)))) & 0xdfdfu) == 0x4e49u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)6) & 0xdfu) == 0x47u))
                     {
                         flag = 0x200L;
                         values = ref _headers._Warning;
+                        nameStr = HeaderNames.Warning;
                     }
                     break;
                 case 8:
@@ -6402,11 +6424,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x100000000L;
                         values = ref _headers._IfMatch;
+                        nameStr = HeaderNames.IfMatch;
                     }
                     else if ((firstTerm8 == 0x45474e41522d4649uL))
                     {
                         flag = 0x800000000L;
                         values = ref _headers._IfRange;
+                        nameStr = HeaderNames.IfRange;
                     }
                     break;
                 case 9:
@@ -6414,6 +6438,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x40000000000L;
                         values = ref _headers._Translate;
+                        nameStr = HeaderNames.Translate;
                     }
                     break;
                 case 10:
@@ -6421,31 +6446,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x80000L;
                         values = ref _headers._Authority;
+                        nameStr = HeaderNames.Authority;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xdfdfdfdfdfdfdfdfuL) == 0x495443454e4e4f43uL) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(4 * sizeof(ushort)))) & 0xdfdfu) == 0x4e4fu))
                     {
                         flag = 0x2L;
                         values = ref _headers._Connection;
+                        nameStr = HeaderNames.Connection;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xdfdfdfffdfdfdfdfuL) == 0x4547412d52455355uL) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(4 * sizeof(ushort)))) & 0xdfdfu) == 0x544eu))
                     {
                         flag = 0x80000000000L;
                         values = ref _headers._UserAgent;
+                        nameStr = HeaderNames.UserAgent;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xdfdfdfffdfdfdfdfuL) == 0x494c412d5045454buL) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(4 * sizeof(ushort)))) & 0xdfdfu) == 0x4556u))
                     {
                         flag = 0x8L;
                         values = ref _headers._KeepAlive;
+                        nameStr = HeaderNames.KeepAlive;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xffdfdfdfdfdfdfdfuL) == 0x2d54534555514552uL) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(4 * sizeof(ushort)))) & 0xdfdfu) == 0x4449u))
                     {
                         flag = 0x400000000000L;
                         values = ref _headers._RequestId;
+                        nameStr = HeaderNames.RequestId;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xdfdfdfdfdfdfdfdfuL) == 0x4154534543415254uL) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(4 * sizeof(ushort)))) & 0xdfdfu) == 0x4554u))
                     {
                         flag = 0x2000000000000L;
                         values = ref _headers._TraceState;
+                        nameStr = HeaderNames.TraceState;
                     }
                     break;
                 case 11:
@@ -6453,11 +6484,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x8000L;
                         values = ref _headers._ContentMD5;
+                        nameStr = HeaderNames.ContentMD5;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xdfdfdfdfdfdfdfdfuL) == 0x5241504543415254uL) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(4 * sizeof(ushort)))) & 0xdfdfu) == 0x4e45u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)10) & 0xdfu) == 0x54u))
                     {
                         flag = 0x1000000000000L;
                         values = ref _headers._TraceParent;
+                        nameStr = HeaderNames.TraceParent;
                     }
                     break;
                 case 12:
@@ -6465,11 +6498,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x800L;
                         values = ref _headers._ContentType;
+                        nameStr = HeaderNames.ContentType;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xdfdfdfdfffdfdfdfuL) == 0x57524f462d58414duL) && ((Unsafe.ReadUnaligned<uint>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(uint)))) & 0xdfdfdfdfu) == 0x53445241u))
                     {
                         flag = 0x2000000000L;
                         values = ref _headers._MaxForwards;
+                        nameStr = HeaderNames.MaxForwards;
                     }
                     break;
                 case 13:
@@ -6477,26 +6512,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x8000000L;
                         values = ref _headers._Authorization;
+                        nameStr = HeaderNames.Authorization;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xdfdfffdfdfdfdfdfuL) == 0x4f432d4548434143uL) && ((Unsafe.ReadUnaligned<uint>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(uint)))) & 0xdfdfdfdfu) == 0x4f52544eu) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)12) & 0xdfu) == 0x4cu))
                     {
                         flag = 0x1L;
                         values = ref _headers._CacheControl;
+                        nameStr = HeaderNames.CacheControl;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xffdfdfdfdfdfdfdfuL) == 0x2d544e45544e4f43uL) && ((Unsafe.ReadUnaligned<uint>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(uint)))) & 0xdfdfdfdfu) == 0x474e4152u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)12) & 0xdfu) == 0x45u))
                     {
                         flag = 0x10000L;
                         values = ref _headers._ContentRange;
+                        nameStr = HeaderNames.ContentRange;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xffdfdfdfdfffdfdfuL) == 0x2d454e4f4e2d4649uL) && ((Unsafe.ReadUnaligned<uint>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(uint)))) & 0xdfdfdfdfu) == 0x4354414du) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)12) & 0xdfu) == 0x48u))
                     {
                         flag = 0x400000000L;
                         values = ref _headers._IfNoneMatch;
+                        nameStr = HeaderNames.IfNoneMatch;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xdfdfdfffdfdfdfdfuL) == 0x444f4d2d5453414cuL) && ((Unsafe.ReadUnaligned<uint>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(uint)))) & 0xdfdfdfdfu) == 0x45494649u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)12) & 0xdfu) == 0x44u))
                     {
                         flag = 0x40000L;
                         values = ref _headers._LastModified;
+                        nameStr = HeaderNames.LastModified;
                     }
                     break;
                 case 14:
@@ -6504,10 +6544,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x1000000L;
                         values = ref _headers._AcceptCharset;
+                        nameStr = HeaderNames.AcceptCharset;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xffdfdfdfdfdfdfdfuL) == 0x2d544e45544e4f43uL) && ((Unsafe.ReadUnaligned<uint>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(uint)))) & 0xdfdfdfdfu) == 0x474e454cu) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(6 * sizeof(ushort)))) & 0xdfdfu) == 0x4854u))
                     {
-                        AppendContentLength(value);
+                        if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector))
+                        {
+                            AppendContentLength(value);
+                        }
+                        else
+                        {
+                            AppendContentLengthCustomEncoding(value, EncodingSelector(HeaderNames.ContentLength));
+                        }
                         return;
                     }
                     break;
@@ -6517,11 +6565,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x2000000L;
                         values = ref _headers._AcceptEncoding;
+                        nameStr = HeaderNames.AcceptEncoding;
                     }
                     else if ((firstTerm15 == 0x4c2d545045434341uL) && ((Unsafe.ReadUnaligned<uint>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(2 * sizeof(uint)))) & 0xdfdfdfdfu) == 0x55474e41u) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(6 * sizeof(ushort)))) & 0xdfdfu) == 0x4741u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)14) & 0xdfu) == 0x45u))
                     {
                         flag = 0x4000000L;
                         values = ref _headers._AcceptLanguage;
+                        nameStr = HeaderNames.AcceptLanguage;
                     }
                     break;
                 case 16:
@@ -6532,16 +6582,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                         {
                             flag = 0x1000L;
                             values = ref _headers._ContentEncoding;
+                            nameStr = HeaderNames.ContentEncoding;
                         }
                         else if (((Unsafe.ReadUnaligned<ulong>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)sizeof(ulong))) & 0xdfdfdfdfdfdfdfdfuL) == 0x45474155474e414cuL))
                         {
                             flag = 0x2000L;
                             values = ref _headers._ContentLanguage;
+                            nameStr = HeaderNames.ContentLanguage;
                         }
                         else if (((Unsafe.ReadUnaligned<ulong>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)sizeof(ulong))) & 0xdfdfdfdfdfdfdfdfuL) == 0x4e4f495441434f4cuL))
                         {
                             flag = 0x4000L;
                             values = ref _headers._ContentLocation;
+                            nameStr = HeaderNames.ContentLocation;
                         }
                     }
                     break;
@@ -6550,11 +6603,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x200000000L;
                         values = ref _headers._IfModifiedSince;
+                        nameStr = HeaderNames.IfModifiedSince;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xdfdfdfdfdfdfdfdfuL) == 0x524546534e415254uL) && ((Unsafe.ReadUnaligned<ulong>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)sizeof(ulong))) & 0xdfdfdfdfdfdfdfffuL) == 0x4e49444f434e452duL) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)16) & 0xdfu) == 0x47u))
                     {
                         flag = 0x40L;
                         values = ref _headers._TransferEncoding;
+                        nameStr = HeaderNames.TransferEncoding;
                     }
                     break;
                 case 19:
@@ -6562,16 +6617,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x800000000000L;
                         values = ref _headers._CorrelationContext;
+                        nameStr = HeaderNames.CorrelationContext;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xdfdfdfdfdfffdfdfuL) == 0x444f4d4e552d4649uL) && ((Unsafe.ReadUnaligned<ulong>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)sizeof(ulong))) & 0xdfdfffdfdfdfdfdfuL) == 0x49532d4445494649uL) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(8 * sizeof(ushort)))) & 0xdfdfu) == 0x434eu) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)18) & 0xdfu) == 0x45u))
                     {
                         flag = 0x1000000000L;
                         values = ref _headers._IfUnmodifiedSince;
+                        nameStr = HeaderNames.IfUnmodifiedSince;
                     }
                     else if (((Unsafe.ReadUnaligned<ulong>(ref nameStart) & 0xdfdfffdfdfdfdfdfuL) == 0x55412d59584f5250uL) && ((Unsafe.ReadUnaligned<ulong>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)sizeof(ulong))) & 0xdfdfdfdfdfdfdfdfuL) == 0x54415a49524f4854uL) && ((Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref nameStart, (IntPtr)(8 * sizeof(ushort)))) & 0xdfdfu) == 0x4f49u) && ((Unsafe.AddByteOffset(ref nameStart, (IntPtr)18) & 0xdfu) == 0x4eu))
                     {
                         flag = 0x4000000000L;
                         values = ref _headers._ProxyAuthorization;
+                        nameStr = HeaderNames.ProxyAuthorization;
                     }
                     break;
                 case 25:
@@ -6579,6 +6637,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x200000000000L;
                         values = ref _headers._UpgradeInsecureRequests;
+                        nameStr = HeaderNames.UpgradeInsecureRequests;
                     }
                     break;
                 case 29:
@@ -6586,6 +6645,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x8000000000000L;
                         values = ref _headers._AccessControlRequestMethod;
+                        nameStr = HeaderNames.AccessControlRequestMethod;
                     }
                     break;
                 case 30:
@@ -6593,6 +6653,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     {
                         flag = 0x10000000000000L;
                         values = ref _headers._AccessControlRequestHeaders;
+                        nameStr = HeaderNames.AccessControlRequestHeaders;
                     }
                     break;
             }
@@ -6622,7 +6683,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                 }
 
                 // We didn't have a previous matching header value, or have already added a header, so get the string for this value.
-                var valueStr = value.GetRequestHeaderStringNonNullCharacters(UseLatin1);
+                var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector);
                 if ((_bits & flag) == 0)
                 {
                     // We didn't already have a header set, so add a new one.
@@ -6640,8 +6701,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                 // The header was not one of the "known" headers.
                 // Convert value to string first, because passing two spans causes 8 bytes stack zeroing in 
                 // this method with rep stosd, which is slower than necessary.
-                var valueStr = value.GetRequestHeaderStringNonNullCharacters(UseLatin1);
-                AppendUnknownHeaders(name, valueStr);
+                nameStr = name.GetHeaderName();
+                var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector);
+                AppendUnknownHeaders(nameStr, valueStr);
             }
         }
 
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
index 1fe3f1f7531..13065f095e4 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
@@ -369,7 +369,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             ConnectionIdFeature = ConnectionId;
 
             HttpRequestHeaders.Reset();
-            HttpRequestHeaders.UseLatin1 = ServerOptions.Latin1RequestHeaders;
+            HttpRequestHeaders.EncodingSelector = ServerOptions.RequestHeaderEncodingSelector;
             HttpRequestHeaders.ReuseHeaderValues = !ServerOptions.DisableStringReuse;
             HttpResponseHeaders.Reset();
             RequestHeaders = HttpRequestHeaders;
@@ -532,7 +532,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             }
 
             string key = name.GetHeaderName();
-            var valueStr = value.GetRequestHeaderStringNonNullCharacters(ServerOptions.Latin1RequestHeaders);
+            var valueStr = value.GetRequestHeaderString(key, HttpRequestHeaders.EncodingSelector);
             RequestTrailers.Append(key, valueStr);
         }
 
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs
index c2a75857c2b..bfc135c34a2 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs
@@ -5,7 +5,9 @@ using System;
 using System.Buffers.Text;
 using System.Collections;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Runtime.CompilerServices;
+using System.Text;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
 using Microsoft.Extensions.Primitives;
 using Microsoft.Net.Http.Headers;
@@ -17,12 +19,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
         private long _previousBits = 0;
 
         public bool ReuseHeaderValues { get; set; }
-        public bool UseLatin1 { get; set; }
+        public Func<string, Encoding> EncodingSelector { get; set; }
 
-        public HttpRequestHeaders(bool reuseHeaderValues = true, bool useLatin1 = false)
+        public HttpRequestHeaders(bool reuseHeaderValues = true, Func<string, Encoding> encodingSelector = null)
         {
             ReuseHeaderValues = reuseHeaderValues;
-            UseLatin1 = useLatin1;
+            EncodingSelector = encodingSelector ?? KestrelServerOptions.DefaultRequestHeaderEncodingSelector;
         }
 
         public void OnHeadersComplete()
@@ -87,7 +89,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                 parsed < 0 ||
                 consumed != value.Length)
             {
-                KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetRequestHeaderStringNonNullCharacters(UseLatin1));
+                KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetRequestHeaderString(HeaderNames.ContentLength, EncodingSelector));
+            }
+
+            _contentLength = parsed;
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private void AppendContentLengthCustomEncoding(ReadOnlySpan<byte> value, Encoding customEncoding)
+        {
+            if (_contentLength.HasValue)
+            {
+                KestrelBadHttpRequestException.Throw(RequestRejectionReason.MultipleContentLengths);
+            }
+
+            // long.MaxValue = 9223372036854775807 (19 chars)
+            Span<char> decodedChars = stackalloc char[20];
+            var numChars = customEncoding.GetChars(value, decodedChars);
+            long parsed = -1;
+
+            if (numChars > 19 ||
+                !long.TryParse(decodedChars.Slice(0, numChars), NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ||
+                parsed < 0)
+            {
+                KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetRequestHeaderString(HeaderNames.ContentLength, EncodingSelector));
             }
 
             _contentLength = parsed;
@@ -108,11 +133,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
         }
 
         [MethodImpl(MethodImplOptions.NoInlining)]
-        private unsafe void AppendUnknownHeaders(ReadOnlySpan<byte> name, string valueString)
+        private unsafe void AppendUnknownHeaders(string name, string valueString)
         {
-            string key = name.GetHeaderName();
-            Unknown.TryGetValue(key, out var existing);
-            Unknown[key] = AppendValue(existing, valueString);
+            Unknown.TryGetValue(name, out var existing);
+            Unknown[name] = AppendValue(existing, valueString);
         }
 
         public Enumerator GetEnumerator()
diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs
index ede9d52e2ea..56235dd2a29 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs
@@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
         private const ulong _http10VersionLong = 3471766442030158920; // GetAsciiStringAsLong("HTTP/1.0"); const results in better codegen
         private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen
 
-        private static readonly UTF8EncodingSealed HeaderValueEncoding = new UTF8EncodingSealed();
+        private static readonly UTF8EncodingSealed DefaultRequestHeaderEncoding = new UTF8EncodingSealed();
         private static readonly SpanAction<char, IntPtr> _getHeaderName = GetHeaderName;
         private static readonly SpanAction<char, IntPtr> _getAsciiStringNonNullCharacters = GetAsciiStringNonNullCharacters;
 
@@ -120,11 +120,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
             }
         }
 
-        public static string GetAsciiOrUTF8StringNonNullCharacters(this Span<byte> span)
-            => GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan<byte>)span);
-
         public static string GetAsciiOrUTF8StringNonNullCharacters(this ReadOnlySpan<byte> span)
-            => StringUtilities.GetAsciiOrUTF8StringNonNullCharacters(span, HeaderValueEncoding);
+            => StringUtilities.GetAsciiOrUTF8StringNonNullCharacters(span, DefaultRequestHeaderEncoding);
 
         private static unsafe void GetAsciiStringNonNullCharacters(Span<char> buffer, IntPtr state)
         {
@@ -139,8 +136,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
             }
         }
 
-        public static string GetRequestHeaderStringNonNullCharacters(this ReadOnlySpan<byte> span, bool useLatin1) =>
-            useLatin1 ? span.GetLatin1StringNonNullCharacters() : span.GetAsciiOrUTF8StringNonNullCharacters(HeaderValueEncoding);
+        public static string GetRequestHeaderString(this ReadOnlySpan<byte> span, string name, Func<string, Encoding> encodingSelector)
+        {
+            if (ReferenceEquals(KestrelServerOptions.DefaultRequestHeaderEncodingSelector, encodingSelector))
+            {
+                return span.GetAsciiOrUTF8StringNonNullCharacters(DefaultRequestHeaderEncoding);
+            }
+
+            var encoding = encodingSelector(name);
+
+            if (encoding is null)
+            {
+                return span.GetAsciiOrUTF8StringNonNullCharacters(DefaultRequestHeaderEncoding);
+            }
+
+            if (ReferenceEquals(encoding, Encoding.Latin1))
+            {
+                return span.GetLatin1StringNonNullCharacters();
+            }
+
+            try
+            {
+                return encoding.GetString(span);
+            }
+            catch (DecoderFallbackException ex)
+            {
+                throw new InvalidOperationException(ex.Message, ex);
+            }
+        }
 
         public static string GetAsciiStringEscaped(this ReadOnlySpan<byte> span, int maxChars)
         {
diff --git a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
index 193d07ca320..ad7210b6d65 100644
--- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
+++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
@@ -255,8 +255,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel
 
             ConfigurationReader = new ConfigurationReader(Configuration);
 
-            Options.Latin1RequestHeaders = ConfigurationReader.Latin1RequestHeaders;
-
             LoadDefaultCert(ConfigurationReader);
 
             foreach (var endpoint in ConfigurationReader.Endpoints)
diff --git a/src/Servers/Kestrel/Core/src/KestrelServer.cs b/src/Servers/Kestrel/Core/src/KestrelServer.cs
index 16d9e34f013..e38860ad4eb 100644
--- a/src/Servers/Kestrel/Core/src/KestrelServer.cs
+++ b/src/Servers/Kestrel/Core/src/KestrelServer.cs
@@ -35,12 +35,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
 
         private IDisposable _configChangedRegistration;
 
-        public KestrelServer(IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, ILoggerFactory loggerFactory)
+        public KestrelServer(
+            IOptions<KestrelServerOptions> options,
+            IEnumerable<IConnectionListenerFactory> transportFactories,
+            ILoggerFactory loggerFactory)
             : this(transportFactories, null, CreateServiceContext(options, loggerFactory))
         {
         }
 
-        public KestrelServer(IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories, ILoggerFactory loggerFactory)
+        public KestrelServer(
+            IOptions<KestrelServerOptions> options,
+            IEnumerable<IConnectionListenerFactory> transportFactories,
+            IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
+            ILoggerFactory loggerFactory)
             : this(transportFactories, multiplexedFactories, CreateServiceContext(options, loggerFactory))
         {
         }
@@ -52,7 +59,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
         }
 
         // For testing
-        internal KestrelServer(IEnumerable<IConnectionListenerFactory> transportFactories, IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories, ServiceContext serviceContext)
+        internal KestrelServer(
+            IEnumerable<IConnectionListenerFactory> transportFactories,
+            IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories,
+            ServiceContext serviceContext)
         {
             if (transportFactories == null)
             {
diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
index e643478c11f..917c1c8a75f 100644
--- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
+++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
@@ -7,6 +7,7 @@ using System.IO;
 using System.Linq;
 using System.Net;
 using System.Security.Cryptography.X509Certificates;
+using System.Text;
 using Microsoft.AspNetCore.Certificates.Generation;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
@@ -22,6 +23,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
     /// </summary>
     public class KestrelServerOptions
     {
+        // internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged.
+        internal static readonly Func<string, Encoding> DefaultRequestHeaderEncodingSelector = _ => null;
+
+        private Func<string, Encoding> _requestHeaderEncodingSelector = DefaultRequestHeaderEncodingSelector;
+
         // The following two lists configure the endpoints that Kestrel should listen to. If both lists are empty, the "urls" config setting (e.g. UseUrls) is used.
         internal List<ListenOptions> CodeBackedListenOptions { get; } = new List<ListenOptions>();
         internal List<ListenOptions> ConfigurationBackedListenOptions { get; } = new List<ListenOptions>();
@@ -65,6 +71,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
         /// </remarks>
         public bool DisableStringReuse { get; set; } = false;
 
+        /// <summary>
+        /// Controls whether to return the AltSvcHeader from on an HTTP/2 or lower response for HTTP/3
+        /// </summary>
+        /// <remarks>
+        /// Defaults to false.
+        /// </remarks>
+        public bool EnableAltSvc { get; set; } = false;
+
+        /// <summary>
+        /// Gets or sets a callback that returns the <see cref="Encoding"/> to decode the value for the specified request header name,
+        /// or <see langword="null"/> to use the default <see cref="UTF8Encoding"/>.
+        /// </summary>
+        public Func<string, Encoding> RequestHeaderEncodingSelector
+        {
+            get => _requestHeaderEncodingSelector;
+            set => _requestHeaderEncodingSelector = value ?? throw new ArgumentNullException(nameof(value));
+        }
+
         /// <summary>
         /// Enables the Listen options callback to resolve and use services registered by the application during startup.
         /// Typically initialized by UseKestrel()"/>.
@@ -78,15 +102,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
 
         /// <summary>
         /// Provides a configuration source where endpoints will be loaded from on server start.
-        /// The default is null.
+        /// The default is <see langword="null"/>.
         /// </summary>
         public KestrelConfigurationLoader ConfigurationLoader { get; set; }
 
-        /// <summary>
-        /// Controls whether to return the AltSvcHeader from on an HTTP/2 or lower response for HTTP/3
-        /// </summary>
-        public bool EnableAltSvc { get; set; } = false;
-
         /// <summary>
         /// A default configuration action for all endpoints. Use for Listen, configuration, the default url, and URLs.
         /// </summary>
@@ -107,11 +126,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
         /// </summary>
         internal bool IsDevCertLoaded { get; set; }
 
-        /// <summary>
-        /// Treat request headers as Latin-1 or ISO/IEC 8859-1 instead of UTF-8.
-        /// </summary>
-        internal bool Latin1RequestHeaders { get; set; }
-
         /// <summary>
         /// Specifies a configuration Action to run for each newly created endpoint. Calling this again will replace
         /// the prior action.
@@ -159,7 +173,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
             if (DefaultCertificate == null && !IsDevCertLoaded)
             {
                 IsDevCertLoaded = true; // Only try once
-                var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
+                var logger = ApplicationServices!.GetRequiredService<ILogger<KestrelServer>>();
                 try
                 {
                     DefaultCertificate = CertificateManager.Instance.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true)
@@ -220,7 +234,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
         /// </summary>
         /// <param name="config">The configuration section for Kestrel.</param>
         /// <param name="reloadOnChange">
-        /// If <see langword="true" />, Kestrel will dynamically update endpoint bindings when configuration changes.
+        /// If <see langword="true"/>, Kestrel will dynamically update endpoint bindings when configuration changes.
         /// This will only reload endpoints defined in the "Endpoints" section of your <paramref name="config"/>. Endpoints defined in code will not be reloaded.
         /// </param>
         /// <returns>A <see cref="KestrelConfigurationLoader"/> for further endpoint configuration.</returns>
diff --git a/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs b/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs
index 098f6fc0ed7..93d28dea749 100644
--- a/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs
+++ b/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs
@@ -8,6 +8,7 @@ using System.Text;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
 using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
 using Xunit;
 using static CodeGenerator.KnownHeaders;
 
@@ -307,11 +308,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             var headers = new HttpRequestHeaders();
             const string key = "\u00141\u00F3d\017c";
 
-            var encoding = Encoding.GetEncoding("iso-8859-1");
 #pragma warning disable CS0618 // Type or member is obsolete
             var exception = Assert.Throws<BadHttpRequestException>(
 #pragma warning restore CS0618 // Type or member is obsolete
-                () => headers.Append(encoding.GetBytes(key), Encoding.ASCII.GetBytes("value")));
+                () => headers.Append(Encoding.Latin1.GetBytes(key), Encoding.ASCII.GetBytes("value")));
             Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode);
         }
 
@@ -473,7 +473,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
                     Assert.Throws<InvalidOperationException>(() =>
                     {
                         var headerName = Encoding.ASCII.GetBytes(header.Name).AsSpan();
-                        var nextSpan = Encoding.GetEncoding("iso-8859-1").GetBytes(headerValueUtf16Latin1CrossOver).AsSpan();
+                        var nextSpan = Encoding.Latin1.GetBytes(headerValueUtf16Latin1CrossOver).AsSpan();
 
                         Assert.False(nextSpan.SequenceEqual(Encoding.ASCII.GetBytes(headerValueUtf16Latin1CrossOver)));
 
@@ -490,7 +490,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         [MemberData(nameof(KnownRequestHeaders))]
         public void Latin1ValuesAcceptedInLatin1ModeButNotReused(bool reuseValue, KnownHeader header)
         {
-            var headers = new HttpRequestHeaders(reuseHeaderValues: reuseValue, useLatin1: true);
+            var headers = new HttpRequestHeaders(reuseHeaderValues: reuseValue, _ => Encoding.Latin1);
 
             var headerValue = new char[127]; // 64 + 32 + 16 + 8 + 4 + 2 + 1
             for (var i = 0; i < headerValue.Length; i++)
@@ -517,19 +517,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
                         headerValueUtf16Latin1CrossOver = new string(headerValue.AsSpan().Slice(0, i + 1));
                     }
 
-                    headers.Reset();
-
                     var headerName = Encoding.ASCII.GetBytes(header.Name).AsSpan();
-                    var latinValueSpan = Encoding.GetEncoding("iso-8859-1").GetBytes(headerValueUtf16Latin1CrossOver).AsSpan();
+                    var latinValueSpan = Encoding.Latin1.GetBytes(headerValueUtf16Latin1CrossOver).AsSpan();
 
                     Assert.False(latinValueSpan.SequenceEqual(Encoding.ASCII.GetBytes(headerValueUtf16Latin1CrossOver)));
 
+                    headers.Reset();
+                    headers.Append(headerName, latinValueSpan);
+                    headers.OnHeadersComplete();
+                    var parsedHeaderValue1 = ((IHeaderDictionary)headers)[header.Name].ToString();
+
+                    headers.Reset();
                     headers.Append(headerName, latinValueSpan);
                     headers.OnHeadersComplete();
-                    var parsedHeaderValue = ((IHeaderDictionary)headers)[header.Name].ToString();
+                    var parsedHeaderValue2 = ((IHeaderDictionary)headers)[header.Name].ToString();
 
-                    Assert.Equal(headerValueUtf16Latin1CrossOver, parsedHeaderValue);
-                    Assert.NotSame(headerValueUtf16Latin1CrossOver, parsedHeaderValue);
+                    Assert.Equal(headerValueUtf16Latin1CrossOver, parsedHeaderValue1);
+                    Assert.Equal(parsedHeaderValue1, parsedHeaderValue2);
+                    Assert.NotSame(parsedHeaderValue1, parsedHeaderValue2);
                 }
 
                 // Reset back to Ascii
@@ -541,7 +546,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         [MemberData(nameof(KnownRequestHeaders))]
         public void NullCharactersRejectedInUTF8AndLatin1Mode(bool useLatin1, KnownHeader header)
         {
-            var headers = new HttpRequestHeaders(useLatin1: useLatin1);
+            var headers = new HttpRequestHeaders(encodingSelector: useLatin1 ? _ => Encoding.Latin1 : (Func<string, Encoding>)null);
 
             var valueArray = new char[127]; // 64 + 32 + 16 + 8 + 4 + 2 + 1
             for (var i = 0; i < valueArray.Length; i++)
@@ -569,6 +574,53 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             }
         }
 
+        [Fact]
+        public void CanSpecifyEncodingBasedOnHeaderName()
+        {
+            const string headerValue = "Hello \u03a0";
+            var acceptNameBytes = Encoding.ASCII.GetBytes(HeaderNames.Accept);
+            var cookieNameBytes = Encoding.ASCII.GetBytes(HeaderNames.Cookie);
+            var headerValueBytes = Encoding.UTF8.GetBytes(headerValue);
+
+            var headers = new HttpRequestHeaders(encodingSelector: headerName =>
+            {
+                // For known headers, the HeaderNames value is passed in.
+                if (ReferenceEquals(headerName, HeaderNames.Accept))
+                {
+                    return Encoding.GetEncoding("ASCII", EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
+                }
+
+                return Encoding.UTF8;
+            });
+
+            Assert.Throws<InvalidOperationException>(() => headers.Append(acceptNameBytes, headerValueBytes));
+            headers.Append(cookieNameBytes, headerValueBytes);
+            headers.OnHeadersComplete();
+
+            var parsedAcceptHeaderValue = ((IHeaderDictionary)headers)[HeaderNames.Accept].ToString();
+            var parsedCookieHeaderValue = ((IHeaderDictionary)headers)[HeaderNames.Cookie].ToString();
+
+            Assert.Empty(parsedAcceptHeaderValue);
+            Assert.Equal(headerValue, parsedCookieHeaderValue);
+        }
+
+        [Fact]
+        public void CanSpecifyEncodingForContentLength()
+        {
+            var contentLengthNameBytes = Encoding.ASCII.GetBytes(HeaderNames.ContentLength);
+            // Always 32 bits per code point, so not a superset of ASCII
+            var contentLengthValueBytes = Encoding.UTF32.GetBytes("1337");
+
+            var headers = new HttpRequestHeaders(encodingSelector: _ => Encoding.UTF32);
+            headers.Append(contentLengthNameBytes, contentLengthValueBytes);
+            headers.OnHeadersComplete();
+
+            Assert.Equal(1337, headers.ContentLength);
+
+            Assert.Throws<InvalidOperationException>(() =>
+                new HttpRequestHeaders().Append(contentLengthNameBytes, contentLengthValueBytes));
+        }
+
         [Fact]
         public void ValueReuseNeverWhenUnknownHeader()
         {
diff --git a/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs
index f070266ec53..956859b2796 100644
--- a/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs
+++ b/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs
@@ -1,6 +1,7 @@
 // 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.Net;
 using Xunit;
 
@@ -60,5 +61,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             // https://github.com/dotnet/aspnetcore/issues/21423
             options.ListenLocalhost(5000);
         }
+
+        [Fact]
+        public void SettingRequestHeaderEncodingSelecterThrowsArgumentNullException()
+        {
+            var options = new KestrelServerOptions();
+
+            var ex = Assert.Throws<ArgumentNullException>(() => options.RequestHeaderEncodingSelector = null);
+            Assert.Equal("value", ex.ParamName);
+        }
     }
 }
diff --git a/src/Servers/Kestrel/Core/test/UTF8Decoding.cs b/src/Servers/Kestrel/Core/test/UTF8Decoding.cs
index 35043d8322d..5083ba7d16f 100644
--- a/src/Servers/Kestrel/Core/test/UTF8Decoding.cs
+++ b/src/Servers/Kestrel/Core/test/UTF8Decoding.cs
@@ -5,6 +5,7 @@ using System;
 using System.Linq;
 using System.Numerics;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
+using Microsoft.Net.Http.Headers;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
@@ -17,7 +18,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         [InlineData(new byte[] { 0xef, 0xbf, 0xbd })] // 3 bytes: Replacement character, highest UTF-8 character currently encoded in the UTF-8 code page
         private void FullUTF8RangeSupported(byte[] encodedBytes)
         {
-            var s = HttpUtilities.GetRequestHeaderStringNonNullCharacters(encodedBytes.AsSpan(), useLatin1: false);
+            var s = HttpUtilities.GetRequestHeaderString(encodedBytes.AsSpan(), HeaderNames.Accept, KestrelServerOptions.DefaultRequestHeaderEncodingSelector);
 
             Assert.Equal(1, s.Length);
         }
@@ -35,7 +36,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
                     var byteRange = Enumerable.Range(1, length).Select(x => (byte)x).ToArray();
                     Array.Copy(bytes, 0, byteRange, position, bytes.Length);
 
-                    Assert.Throws<InvalidOperationException>(() => HttpUtilities.GetRequestHeaderStringNonNullCharacters(byteRange.AsSpan(), useLatin1: false));
+                    Assert.Throws<InvalidOperationException>(() =>
+                        HttpUtilities.GetRequestHeaderString(byteRange.AsSpan(), HeaderNames.Accept, KestrelServerOptions.DefaultRequestHeaderEncodingSelector));
                 }
             }
         }
diff --git a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs
index 8d93023a467..fb0b182cdd2 100644
--- a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs
+++ b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs
@@ -7,6 +7,7 @@ using System.IO;
 using System.Linq;
 using System.Security.Authentication;
 using System.Security.Cryptography.X509Certificates;
+using System.Text;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
 using Microsoft.AspNetCore.Server.Kestrel.Https;
@@ -587,27 +588,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests
             Assert.True(ran1);
         }
 
-        [Fact]
-        public void Latin1RequestHeadersReadFromConfig()
-        {
-            var options = CreateServerOptions();
-            var config =  new ConfigurationBuilder().AddInMemoryCollection().Build();
-
-            Assert.False(options.Latin1RequestHeaders);
-            options.Configure(config).Load();
-            Assert.False(options.Latin1RequestHeaders);
-
-            options = CreateServerOptions();
-            config = new ConfigurationBuilder().AddInMemoryCollection(new[]
-            {
-                new KeyValuePair<string, string>("Latin1RequestHeaders", "true"),
-            }).Build();
-
-            Assert.False(options.Latin1RequestHeaders);
-            options.Configure(config).Load();
-            Assert.True(options.Latin1RequestHeaders);
-        }
-
         [Fact]
         public void Reload_IdentifiesEndpointsToStartAndStop()
         {
diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/BytesToStringBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/BytesToStringBenchmark.cs
index cd6d19b75c9..a38bf2dc48a 100644
--- a/src/Servers/Kestrel/perf/Kestrel.Performance/BytesToStringBenchmark.cs
+++ b/src/Servers/Kestrel/perf/Kestrel.Performance/BytesToStringBenchmark.cs
@@ -2,7 +2,9 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using BenchmarkDotNet.Attributes;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
+using Microsoft.Net.Http.Headers;
 
 namespace Microsoft.AspNetCore.Server.Kestrel.Performance
 {
@@ -10,6 +12,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
     {
         private const int Iterations = 50;
 
+        private string _headerName;
         private byte[] _asciiBytes;
         private byte[] _utf8Bytes;
 
@@ -27,24 +30,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
             switch (Type)
             {
                 case BenchmarkTypes.KeepAlive:
+                    _headerName = HeaderNames.Connection;
                     // keep-alive
                     _asciiBytes = new byte[] { 0x6b, 0x65, 0x65, 0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65 };
                     // kéép-álivé
                     _utf8Bytes = new byte[] { 0x6b, 0xc3, 0xa9, 0xc3, 0xa9, 0x70, 0x2d, 0xc3, 0xa1, 0x6c, 0x69, 0x76, 0xc3, 0xa9 };
                     break;
                 case BenchmarkTypes.Accept:
+                    _headerName = HeaderNames.Accept;
                     // text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7
                     _asciiBytes = new byte[] { 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x71, 0x3d, 0x30, 0x2e, 0x39, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, 0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c, 0x3b, 0x71, 0x3d, 0x30, 0x2e, 0x39, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, 0x6d, 0x6c, 0x3b, 0x71, 0x3d, 0x30, 0x2e, 0x38, 0x2c, 0x2a, 0x2f, 0x2a, 0x3b, 0x71, 0x3d, 0x30, 0x2e, 0x37 };
                     // téxt/pláin,téxt/html;q=0.9,ápplicátion/xhtml+xml;q=0.9,ápplicátion/xml;q=0.8,*/*;q=0.7
                     _utf8Bytes = new byte[] { 0x74, 0xc3, 0xa9, 0x78, 0x74, 0x2f, 0x70, 0x6c, 0xc3, 0xa1, 0x69, 0x6e, 0x2c, 0x74, 0xc3, 0xa9, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x71, 0x3d, 0x30, 0x2e, 0x39, 0x2c, 0xc3, 0xa1, 0x70, 0x70, 0x6c, 0x69, 0x63, 0xc3, 0xa1, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, 0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c, 0x3b, 0x71, 0x3d, 0x30, 0x2e, 0x39, 0x2c, 0xc3, 0xa1, 0x70, 0x70, 0x6c, 0x69, 0x63, 0xc3, 0xa1, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, 0x6d, 0x6c, 0x3b, 0x71, 0x3d, 0x30, 0x2e, 0x38, 0x2c, 0x2a, 0x2f, 0x2a, 0x3b, 0x71, 0x3d, 0x30, 0x2e, 0x37 };
                     break;
                 case BenchmarkTypes.UserAgent:
+                    _headerName = HeaderNames.UserAgent;
                     // Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36
                     _asciiBytes = new byte[] { 0x4d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0x61, 0x2f, 0x35, 0x2e, 0x30, 0x20, 0x28, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x20, 0x4e, 0x54, 0x20, 0x31, 0x30, 0x2e, 0x30, 0x3b, 0x20, 0x57, 0x4f, 0x57, 0x36, 0x34, 0x29, 0x20, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x57, 0x65, 0x62, 0x4b, 0x69, 0x74, 0x2f, 0x35, 0x33, 0x37, 0x2e, 0x33, 0x36, 0x20, 0x28, 0x4b, 0x48, 0x54, 0x4d, 0x4c, 0x2c, 0x20, 0x6c, 0x69, 0x6b, 0x65, 0x20, 0x47, 0x65, 0x63, 0x6b, 0x6f, 0x29, 0x20, 0x43, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x2f, 0x35, 0x34, 0x2e, 0x30, 0x2e, 0x32, 0x38, 0x34, 0x30, 0x2e, 0x39, 0x39, 0x20, 0x53, 0x61, 0x66, 0x61, 0x72, 0x69, 0x2f, 0x35, 0x33, 0x37, 0x2e, 0x33, 0x36 };
                     // Mozillá/5.0 (Windows NT 10.0; WOW64) áppléWébKit/537.36 (KHTML, liké Gécko) Chromé/54.0.2840.99 Sáfári/537.36
                     _utf8Bytes = new byte[] { 0x4d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0xc3, 0xa1, 0x2f, 0x35, 0x2e, 0x30, 0x20, 0x28, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x20, 0x4e, 0x54, 0x20, 0x31, 0x30, 0x2e, 0x30, 0x3b, 0x20, 0x57, 0x4f, 0x57, 0x36, 0x34, 0x29, 0x20, 0xc3, 0xa1, 0x70, 0x70, 0x6c, 0xc3, 0xa9, 0x57, 0xc3, 0xa9, 0x62, 0x4b, 0x69, 0x74, 0x2f, 0x35, 0x33, 0x37, 0x2e, 0x33, 0x36, 0x20, 0x28, 0x4b, 0x48, 0x54, 0x4d, 0x4c, 0x2c, 0x20, 0x6c, 0x69, 0x6b, 0xc3, 0xa9, 0x20, 0x47, 0xc3, 0xa9, 0x63, 0x6b, 0x6f, 0x29, 0x20, 0x43, 0x68, 0x72, 0x6f, 0x6d, 0xc3, 0xa9, 0x2f, 0x35, 0x34, 0x2e, 0x30, 0x2e, 0x32, 0x38, 0x34, 0x30, 0x2e, 0x39, 0x39, 0x20, 0x53, 0xc3, 0xa1, 0x66, 0xc3, 0xa1, 0x72, 0x69, 0x2f, 0x35, 0x33, 0x37, 0x2e, 0x33, 0x36 };
                     break;
                 case BenchmarkTypes.Cookie:
+                    _headerName = HeaderNames.Cookie;
                     // prov=20629ccd-8b0f-e8ef-2935-cd26609fc0bc; __qca=P0-1591065732-1479167353442; _ga=GA1.2.1298898376.1479167354; _gat=1; sgt=id=9519gfde_3347_4762_8762_df51458c8ec2; acct=t=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric&s=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric
                     _asciiBytes = new byte[] { 0x70, 0x72, 0x6f, 0x76, 0x3d, 0x32, 0x30, 0x36, 0x32, 0x39, 0x63, 0x63, 0x64, 0x2d, 0x38, 0x62, 0x30, 0x66, 0x2d, 0x65, 0x38, 0x65, 0x66, 0x2d, 0x32, 0x39, 0x33, 0x35, 0x2d, 0x63, 0x64, 0x32, 0x36, 0x36, 0x30, 0x39, 0x66, 0x63, 0x30, 0x62, 0x63, 0x3b, 0x20, 0x5f, 0x5f, 0x71, 0x63, 0x61, 0x3d, 0x50, 0x30, 0x2d, 0x31, 0x35, 0x39, 0x31, 0x30, 0x36, 0x35, 0x37, 0x33, 0x32, 0x2d, 0x31, 0x34, 0x37, 0x39, 0x31, 0x36, 0x37, 0x33, 0x35, 0x33, 0x34, 0x34, 0x32, 0x3b, 0x20, 0x5f, 0x67, 0x61, 0x3d, 0x47, 0x41, 0x31, 0x2e, 0x32, 0x2e, 0x31, 0x32, 0x39, 0x38, 0x38, 0x39, 0x38, 0x33, 0x37, 0x36, 0x2e, 0x31, 0x34, 0x37, 0x39, 0x31, 0x36, 0x37, 0x33, 0x35, 0x34, 0x3b, 0x20, 0x5f, 0x67, 0x61, 0x74, 0x3d, 0x31, 0x3b, 0x20, 0x73, 0x67, 0x74, 0x3d, 0x69, 0x64, 0x3d, 0x39, 0x35, 0x31, 0x39, 0x67, 0x66, 0x64, 0x65, 0x5f, 0x33, 0x33, 0x34, 0x37, 0x5f, 0x34, 0x37, 0x36, 0x32, 0x5f, 0x38, 0x37, 0x36, 0x32, 0x5f, 0x64, 0x66, 0x35, 0x31, 0x34, 0x35, 0x38, 0x63, 0x38, 0x65, 0x63, 0x32, 0x3b, 0x20, 0x61, 0x63, 0x63, 0x74, 0x3d, 0x74, 0x3d, 0x77, 0x68, 0x79, 0x2d, 0x69, 0x73, 0x2d, 0x25, 0x65, 0x30, 0x25, 0x61, 0x35, 0x25, 0x61, 0x37, 0x25, 0x65, 0x30, 0x25, 0x61, 0x35, 0x25, 0x61, 0x38, 0x25, 0x65, 0x30, 0x25, 0x61, 0x35, 0x25, 0x61, 0x39, 0x2d, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x26, 0x73, 0x3d, 0x77, 0x68, 0x79, 0x2d, 0x69, 0x73, 0x2d, 0x25, 0x65, 0x30, 0x25, 0x61, 0x35, 0x25, 0x61, 0x37, 0x25, 0x65, 0x30, 0x25, 0x61, 0x35, 0x25, 0x61, 0x38, 0x25, 0x65, 0x30, 0x25, 0x61, 0x35, 0x25, 0x61, 0x39, 0x2d, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63 };
                     // prov=20629ccd-8b0f-é8éf-2935-cd26609fc0bc; __qcá=P0-1591065732-1479167353442; _gá=Gá1.2.1298898376.1479167354; _gát=1; sgt=id=9519gfdé_3347_4762_8762_df51458c8éc2; ácct=t=why-is-%é0%á5%á7%é0%á5%á8%é0%á5%á9-numéric&s=why-is-%é0%á5%á7%é0%á5%á8%é0%á5%á9-numéric
@@ -67,7 +74,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
         {
             for (uint i = 0; i < Iterations; i++)
             {
-                HttpUtilities.GetRequestHeaderStringNonNullCharacters(_utf8Bytes, useLatin1: false);
+                HttpUtilities.GetRequestHeaderString(_utf8Bytes, _headerName, KestrelServerOptions.DefaultRequestHeaderEncodingSelector);
             }
         }
 
diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs
index e6389a675eb..5295f8ce5b9 100644
--- a/src/Servers/Kestrel/shared/KnownHeaders.cs
+++ b/src/Servers/Kestrel/shared/KnownHeaders.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
 using System.Text;
+using Microsoft.Net.Http.Headers;
 
 namespace CodeGenerator
 {
@@ -230,23 +231,39 @@ namespace CodeGenerator
                 firstTermVar = "";
             }
 
+            string GenerateIfBody(KnownHeader header, string extraIndent = "")
+            {
+                if (header.Identifier == "ContentLength")
+                {
+                    return $@"
+                        {extraIndent}if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector))
+                        {extraIndent}{{
+                        {extraIndent}    AppendContentLength(value);
+                        {extraIndent}}}
+                        {extraIndent}else
+                        {extraIndent}{{
+                        {extraIndent}    AppendContentLengthCustomEncoding(value, EncodingSelector(HeaderNames.ContentLength));
+                        {extraIndent}}}
+                        {extraIndent}return;";
+                }
+                else
+                {
+                    return $@"
+                        {extraIndent}flag = {header.FlagBit()};
+                        {extraIndent}values = ref _headers._{header.Identifier};
+                        {extraIndent}nameStr = HeaderNames.{header.Identifier};";
+                }
+            }
+
             var groups = values.GroupBy(header => header.EqualIgnoreCaseBytesFirstTerm());
             return start + $@"{Each(groups,  (byFirstTerm, i) => $@"{(byFirstTerm.Count() == 1 ? $@"{Each(byFirstTerm, header => $@"
                     {(i > 0 ? "else " : "")}if ({header.EqualIgnoreCaseBytes(firstTermVar)})
-                    {{{(header.Identifier == "ContentLength" ? $@"
-                        AppendContentLength(value);
-                        return;" : $@"
-                        flag = {header.FlagBit()};
-                        values = ref _headers._{header.Identifier};")}
+                    {{{GenerateIfBody(header)}
                     }}")}" : $@"
                     if ({byFirstTerm.Key.Replace(firstTermVarExpression, firstTermVar)})
                     {{{Each(byFirstTerm, (header, i) => $@"
                         {(i > 0 ? "else " : "")}if ({header.EqualIgnoreCaseBytesSecondTermOnwards()})
-                        {{{(header.Identifier == "ContentLength" ? $@"
-                            AppendContentLength(value);
-                            return;" : $@"
-                            flag = {header.FlagBit()};
-                            values = ref _headers._{header.Identifier};")}
+                        {{{GenerateIfBody(header, extraIndent: "    ")}
                         }}")}
                     }}")}")}";
         }
@@ -986,6 +1003,7 @@ $@"        private void Clear(long bitsToClear)
         public unsafe void Append(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
         {{
             ref byte nameStart = ref MemoryMarshal.GetReference(name);
+            var nameStr = string.Empty;
             ref StringValues values = ref Unsafe.AsRef<StringValues>(null);
             var flag = 0L;
 
@@ -1017,7 +1035,7 @@ $@"        private void Clear(long bitsToClear)
                 }}
 
                 // We didn't have a previous matching header value, or have already added a header, so get the string for this value.
-                var valueStr = value.GetRequestHeaderStringNonNullCharacters(UseLatin1);
+                var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector);
                 if ((_bits & flag) == 0)
                 {{
                     // We didn't already have a header set, so add a new one.
@@ -1035,8 +1053,9 @@ $@"        private void Clear(long bitsToClear)
                 // The header was not one of the ""known"" headers.
                 // Convert value to string first, because passing two spans causes 8 bytes stack zeroing in 
                 // this method with rep stosd, which is slower than necessary.
-                var valueStr = value.GetRequestHeaderStringNonNullCharacters(UseLatin1);
-                AppendUnknownHeaders(name, valueStr);
+                nameStr = name.GetHeaderName();
+                var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector);
+                AppendUnknownHeaders(nameStr, valueStr);
             }}
         }}" : "")}
 
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
index 56f42251ee6..3ca2b86c14b 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
@@ -4609,7 +4609,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         [Fact]
         public async Task HEADERS_Received_Latin1_AcceptedWhenLatin1OptionIsConfigured()
         {
-            _serviceContext.ServerOptions.Latin1RequestHeaders = true;
+            _serviceContext.ServerOptions.RequestHeaderEncodingSelector = _ => Encoding.Latin1;
 
             await InitializeConnectionAsync(context =>
             {
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
index c379a4f4dd0..adae3b99c0b 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
@@ -406,7 +406,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
 
         void IHttpHeadersHandler.OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
         {
-            _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetRequestHeaderStringNonNullCharacters(useLatin1: _serviceContext.ServerOptions.Latin1RequestHeaders);
+            var nameStr = name.GetHeaderName();
+            _decodedHeaders[nameStr] = value.GetRequestHeaderString(nameStr, _serviceContext.ServerOptions.RequestHeaderEncodingSelector);
         }
 
         void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
index 1419c9cc8ef..07afb75f285 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
@@ -356,7 +356,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             public void OnStaticIndexedHeader(int index)
             {
                 var knownHeader = H3StaticTable.GetHeaderFieldAt(index);
-                _decodedHeaders[((Span<byte>)knownHeader.Name).GetAsciiStringNonNullCharacters()] = HttpUtilities.GetAsciiOrUTF8StringNonNullCharacters(knownHeader.Value);
+                _decodedHeaders[((Span<byte>)knownHeader.Name).GetAsciiStringNonNullCharacters()] = HttpUtilities.GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan<byte>)knownHeader.Value);
             }
 
             public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs
index 3ada9a09a6c..6257858c1c3 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs
@@ -2001,7 +2001,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
         {
             var testContext = new TestServiceContext(LoggerFactory);
 
-            testContext.ServerOptions.Latin1RequestHeaders = true;
+            testContext.ServerOptions.RequestHeaderEncodingSelector = _ => Encoding.Latin1;
 
             await using (var server = new TestServer(context =>
             {
@@ -2059,6 +2059,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
             }
         }
 
+        [Fact]
+        public async Task CustomRequestHeaderEncodingSelectorCanBeConfigured()
+        {
+            var testContext = new TestServiceContext(LoggerFactory);
+
+            testContext.ServerOptions.RequestHeaderEncodingSelector = _ => Encoding.UTF32;
+
+            await using (var server = new TestServer(context =>
+            {
+                Assert.Equal("£", context.Request.Headers["X-Test"]);
+                return Task.CompletedTask;
+            }, testContext))
+            {
+                using (var connection = server.CreateConnection())
+                {
+                    await connection.Send(
+                        "GET / HTTP/1.1",
+                        "Host:",
+                        "X-Test: ");
+
+                    await connection.Stream.WriteAsync(Encoding.UTF32.GetBytes("£")).DefaultTimeout();
+
+                    await connection.Send("",
+                        "",
+                        "");
+
+                    await connection.Receive(
+                        "HTTP/1.1 200 OK",
+                        $"Date: {testContext.DateHeaderValue}",
+                        "Content-Length: 0",
+                        "",
+                        "");
+                }
+            }
+        }
+
         public static TheoryData<string, string> HostHeaderData => HttpParsingData.HostHeaderData;
 
         private class IntAsClass
diff --git a/src/Shared/ServerInfrastructure/StringUtilities.cs b/src/Shared/ServerInfrastructure/StringUtilities.cs
index 8f6b01027a8..0edcea2cf7d 100644
--- a/src/Shared/ServerInfrastructure/StringUtilities.cs
+++ b/src/Shared/ServerInfrastructure/StringUtilities.cs
@@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
 {
     internal static class StringUtilities
     {
+        private static readonly SpanAction<char, IntPtr> s_getAsciiOrUtf8StringNonNullCharacters = GetAsciiStringNonNullCharacters;
+
         private static string GetAsciiOrUTF8StringNonNullCharacters(this Span<byte> span, Encoding defaultEncoding)
             => GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan<byte>)span, defaultEncoding);
 
@@ -52,15 +54,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
             }
         }
 
-        private static readonly SpanAction<char, IntPtr> s_getAsciiOrUtf8StringNonNullCharacters = GetAsciiOrUTF8StringNonNullCharacters;
-
-        private static unsafe void GetAsciiOrUTF8StringNonNullCharacters(Span<char> buffer, IntPtr state)
+        private static unsafe void GetAsciiStringNonNullCharacters(Span<char> buffer, IntPtr state)
         {
             fixed (char* output = &MemoryMarshal.GetReference(buffer))
             {
-                // This version if AsciiUtilities returns null if there are any null (0 byte) characters
-                // in the string
-                if (!StringUtilities.TryGetAsciiString((byte*)state.ToPointer(), output, buffer.Length))
+                // This version if AsciiUtilities returns false if there are any null ('\0') or non-Ascii
+                // character (> 127) in the string.
+                if (!TryGetAsciiString((byte*)state.ToPointer(), output, buffer.Length))
                 {
                     // Mark resultString for UTF-8 encoding
                     output[0] = '\0';
-- 
GitLab