diff --git a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.Manual.cs b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.Manual.cs
index a0d91f86060b908c552ce7da3b4b9d4165d46cae..4dec82338a3aa874629a0f17bfbee4cdb8716fe1 100644
--- a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.Manual.cs
+++ b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.Manual.cs
@@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
     {
         internal System.Security.Cryptography.X509Certificates.X509Certificate2 DefaultCertificate { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
         internal bool IsDevCertLoaded { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+        internal bool Latin1RequestHeaders { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
         internal System.Collections.Generic.List<Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions> ListenOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
         internal void ApplyDefaultCert(Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions httpsOptions) { }
         internal void ApplyEndpointDefaults(Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions listenOptions) { }
@@ -433,6 +434,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
         public System.Collections.Generic.IDictionary<string, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.CertificateConfig> Certificates { get { throw null; } }
         public Microsoft.AspNetCore.Server.Kestrel.Core.Internal.EndpointDefaults EndpointDefaults { get { throw null; } }
         public System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Server.Kestrel.Core.Internal.EndpointConfig> Endpoints { get { throw null; } }
+        public bool Latin1RequestHeaders { get { throw null; } }
     }
     internal partial class HttpConnectionContext
     {
@@ -879,7 +881,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
     }
     internal sealed partial class HttpRequestHeaders : Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders
     {
-        public HttpRequestHeaders(bool reuseHeaderValues = true) { }
+        public HttpRequestHeaders(bool reuseHeaderValues = true, bool useLatin1 = false) { }
         public bool HasConnection { get { throw null; } }
         public bool HasTransferEncoding { get { throw null; } }
         public Microsoft.Extensions.Primitives.StringValues HeaderAccept { get { throw null; } set { } }
@@ -1614,7 +1616,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
         public const string Http2Version = "HTTP/2";
         public const string HttpsUriScheme = "https://";
         public const string HttpUriScheme = "http://";
-        public static string GetAsciiOrUTF8StringNonNullCharacters(this System.Span<byte> span) { throw null; }
         public static string GetAsciiStringEscaped(this System.Span<byte> span, int maxChars) { throw null; }
         public static string GetAsciiStringNonNullCharacters(this System.Span<byte> span) { throw null; }
         public static string GetHeaderName(this System.Span<byte> span) { throw null; }
@@ -1624,6 +1625,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
         public static Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod GetKnownMethod(string value) { throw null; }
         [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]internal unsafe static Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpVersion GetKnownVersion(byte* location, int length) { throw null; }
         [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public static bool GetKnownVersion(this System.Span<byte> span, out Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpVersion knownVersion, out byte length) { throw null; }
+        public static string GetRequestHeaderStringNonNullCharacters(this System.Span<byte> span, bool useLatin1) { throw null; }
         public static bool IsHostHeaderValid(string hostText) { throw null; }
         public static string MethodToString(Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod method) { throw null; }
         public static string SchemeToString(Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpScheme scheme) { throw null; }
@@ -1683,6 +1685,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
         public static bool BytesOrdinalEqualsStringAndAscii(string previousValue, System.Span<byte> newValue) { throw null; }
         public static string ConcatAsHexSuffix(string str, char separator, uint number) { throw null; }
         public unsafe static bool TryGetAsciiString(byte* input, char* output, int count) { throw null; }
+        public unsafe static bool TryGetLatin1String(byte* input, char* output, int count) { throw null; }
     }
     internal partial class TimeoutControl : Microsoft.AspNetCore.Server.Kestrel.Core.Features.IConnectionTimeoutFeature, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.ITimeoutControl
     {
diff --git a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs b/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
index 259f2c61b6810d1cc2a50b28ddcb15c9c0e2155a..79dc84d5c1c134834e8b05dfd2e702b28946999c 100644
--- a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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;
@@ -15,11 +15,13 @@ 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 IConfiguration _configuration;
         private IDictionary<string, CertificateConfig> _certificates;
         private IList<EndpointConfig> _endpoints;
         private EndpointDefaults _endpointDefaults;
+        private bool? _latin1RequestHeaders;
 
         public ConfigurationReader(IConfiguration configuration)
         {
@@ -65,6 +67,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
             }
         }
 
+        public bool Latin1RequestHeaders
+        {
+            get
+            {
+                if (_latin1RequestHeaders is null)
+                {
+                    _latin1RequestHeaders = _configuration.GetValue<bool>(Latin1RequestHeadersKey);
+                }
+
+                return _latin1RequestHeaders.Value;
+            }
+        }
+
         private void ReadCertificates()
         {
             _certificates = new Dictionary<string, CertificateConfig>(0);
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 e626a19d9cf41708e1759d9d5a1914bd082382fb..aa22d87a863122484733852c5207df7620d73e5b 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs
@@ -6105,7 +6105,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.GetAsciiOrUTF8StringNonNullCharacters();
+                var valueStr = value.GetRequestHeaderStringNonNullCharacters(_useLatin1);
                 if ((_bits & flag) == 0)
                 {
                     // We didn't already have a header set, so add a new one.
@@ -6123,7 +6123,7 @@ 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.GetAsciiOrUTF8StringNonNullCharacters();
+                var valueStr = value.GetRequestHeaderStringNonNullCharacters(_useLatin1);
                 AppendUnknownHeaders(name, valueStr);
             }
         }
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
index 7f10c3af64ccbf51a4c0d6392f8299f4eeaeacf7..f2d1564334bbf5e2732daf9f27a98eac64bfc584 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
@@ -77,7 +77,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             _context = context;
 
             ServerOptions = ServiceContext.ServerOptions;
-            HttpRequestHeaders = new HttpRequestHeaders(reuseHeaderValues: !ServerOptions.DisableStringReuse);
+
+            HttpRequestHeaders = new HttpRequestHeaders(
+                reuseHeaderValues: !ServerOptions.DisableStringReuse,
+                useLatin1: ServerOptions.Latin1RequestHeaders);
+
             HttpResponseControl = this;
         }
 
@@ -513,7 +517,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             }
 
             string key = name.GetHeaderName();
-            var valueStr = value.GetAsciiOrUTF8StringNonNullCharacters();
+            var valueStr = value.GetRequestHeaderStringNonNullCharacters(ServerOptions.Latin1RequestHeaders);
             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 32e9e41d84d5aad0ba66c57e941e8d084a3c7e2e..af631f644249de800dc2c6536d1016af94c4be60 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs
@@ -15,11 +15,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
     internal sealed partial class HttpRequestHeaders : HttpHeaders
     {
         private readonly bool _reuseHeaderValues;
+        private readonly bool _useLatin1;
         private long _previousBits = 0;
 
-        public HttpRequestHeaders(bool reuseHeaderValues = true)
+        public HttpRequestHeaders(bool reuseHeaderValues = true, bool useLatin1 = false)
         {
             _reuseHeaderValues = reuseHeaderValues;
+            _useLatin1 = useLatin1;
         }
 
         public void OnHeadersComplete()
@@ -80,7 +82,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                 parsed < 0 ||
                 consumed != value.Length)
             {
-                BadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetAsciiOrUTF8StringNonNullCharacters());
+                BadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetRequestHeaderStringNonNullCharacters(_useLatin1));
             }
 
             _contentLength = parsed;
diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs
index f57c296e1ef0ebb137940d94b37dc97173439b0d..04fd9711d71156d31f17e97e83e31d1e1b75eaf9 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs
@@ -120,7 +120,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
             fixed (char* output = asciiString)
             fixed (byte* buffer = span)
             {
-                // This version if AsciiUtilities returns null if there are any null (0 byte) characters
+                // StringUtilities.TryGetAsciiString returns null if there are any null (0 byte) characters
                 // in the string
                 if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length))
                 {
@@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
             return asciiString;
         }
 
-        public static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this Span<byte> span)
+        private static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this Span<byte> span)
         {
             if (span.IsEmpty)
             {
@@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
             fixed (char* output = resultString)
             fixed (byte* buffer = span)
             {
-                // This version if AsciiUtilities returns null if there are any null (0 byte) characters
+                // StringUtilities.TryGetAsciiString returns null if there are any null (0 byte) characters
                 // in the string
                 if (!StringUtilities.TryGetAsciiString(buffer, output, span.Length))
                 {
@@ -162,9 +162,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
                     }
                 }
             }
+
+            return resultString;
+        }
+
+        private static unsafe string GetLatin1StringNonNullCharacters(this Span<byte> span)
+        {
+            if (span.IsEmpty)
+            {
+                return string.Empty;
+            }
+
+            var resultString = new string('\0', span.Length);
+
+            fixed (char* output = resultString)
+            fixed (byte* buffer = span)
+            {
+                // This returns false if there are any null (0 byte) characters in the string.
+                if (!StringUtilities.TryGetLatin1String(buffer, output, span.Length))
+                {
+                    // null characters are considered invalid
+                    throw new InvalidOperationException();
+                }
+            }
+
             return resultString;
         }
 
+        public static string GetRequestHeaderStringNonNullCharacters(this Span<byte> span, bool useLatin1) =>
+            useLatin1 ? GetLatin1StringNonNullCharacters(span) : GetAsciiOrUTF8StringNonNullCharacters(span);
+
         public static string GetAsciiStringEscaped(this Span<byte> span, int maxChars)
         {
             var sb = new StringBuilder();
diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/StringUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/StringUtilities.cs
index aea8290b66a2e05210f2bfc81cf5df4bd08e1aa9..268d77a0e45241de60d5e0586dba9c3f2d82c98d 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/StringUtilities.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/StringUtilities.cs
@@ -2,7 +2,6 @@
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
-using System.Buffers.Binary;
 using System.Diagnostics;
 using System.Numerics;
 using System.Runtime.CompilerServices;
@@ -17,6 +16,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
         [MethodImpl(MethodImplOptions.AggressiveOptimization)]
         public static unsafe bool TryGetAsciiString(byte* input, char* output, int count)
         {
+            Debug.Assert(input != null);
+            Debug.Assert(output != null);
+
             // Calculate end position
             var end = input + count;
             // Start as valid
@@ -115,6 +117,111 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
             return isValid;
         }
 
+        [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+        public static unsafe bool TryGetLatin1String(byte* input, char* output, int count)
+        {
+            Debug.Assert(input != null);
+            Debug.Assert(output != null);
+
+            // Calculate end position
+            var end = input + count;
+            // Start as valid
+            var isValid = true;
+
+            do
+            {
+                // If Vector not-accelerated or remaining less than vector size
+                if (!Vector.IsHardwareAccelerated || input > end - Vector<sbyte>.Count)
+                {
+                    if (IntPtr.Size == 8) // Use Intrinsic switch for branch elimination
+                    {
+                        // 64-bit: Loop longs by default
+                        while (input <= end - sizeof(long))
+                        {
+                            isValid &= CheckBytesNotNull(((long*)input)[0]);
+
+                            output[0] = (char)input[0];
+                            output[1] = (char)input[1];
+                            output[2] = (char)input[2];
+                            output[3] = (char)input[3];
+                            output[4] = (char)input[4];
+                            output[5] = (char)input[5];
+                            output[6] = (char)input[6];
+                            output[7] = (char)input[7];
+
+                            input += sizeof(long);
+                            output += sizeof(long);
+                        }
+                        if (input <= end - sizeof(int))
+                        {
+                            isValid &= CheckBytesNotNull(((int*)input)[0]);
+
+                            output[0] = (char)input[0];
+                            output[1] = (char)input[1];
+                            output[2] = (char)input[2];
+                            output[3] = (char)input[3];
+
+                            input += sizeof(int);
+                            output += sizeof(int);
+                        }
+                    }
+                    else
+                    {
+                        // 32-bit: Loop ints by default
+                        while (input <= end - sizeof(int))
+                        {
+                            isValid &= CheckBytesNotNull(((int*)input)[0]);
+
+                            output[0] = (char)input[0];
+                            output[1] = (char)input[1];
+                            output[2] = (char)input[2];
+                            output[3] = (char)input[3];
+
+                            input += sizeof(int);
+                            output += sizeof(int);
+                        }
+                    }
+                    if (input <= end - sizeof(short))
+                    {
+                        isValid &= CheckBytesNotNull(((short*)input)[0]);
+
+                        output[0] = (char)input[0];
+                        output[1] = (char)input[1];
+
+                        input += sizeof(short);
+                        output += sizeof(short);
+                    }
+                    if (input < end)
+                    {
+                        isValid &= CheckBytesNotNull(((sbyte*)input)[0]);
+                        output[0] = (char)input[0];
+                    }
+
+                    return isValid;
+                }
+
+                // do/while as entry condition already checked
+                do
+                {
+                    // Use byte/ushort instead of signed equivalents to ensure it doesn't fill based on the high bit.
+                    var vector = Unsafe.AsRef<Vector<byte>>(input);
+                    isValid &= CheckBytesNotNull(vector);
+                    Vector.Widen(
+                        vector,
+                        out Unsafe.AsRef<Vector<ushort>>(output),
+                        out Unsafe.AsRef<Vector<ushort>>(output + Vector<ushort>.Count));
+
+                    input += Vector<byte>.Count;
+                    output += Vector<byte>.Count;
+                } while (input <= end - Vector<byte>.Count);
+
+                // Vector path done, loop back to do non-Vector
+                // If is a exact multiple of vector size, bail now
+            } while (input < end);
+
+            return isValid;
+        }
+
         [MethodImpl(MethodImplOptions.AggressiveOptimization)]
         public unsafe static bool BytesOrdinalEqualsStringAndAscii(string previousValue, Span<byte> newValue)
         {
@@ -421,7 +528,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
         // Validate: bytes != 0 && bytes <= 127
         //  Subtract 1 from all bytes to move 0 to high bits
         //  bitwise or with self to catch all > 127 bytes
-        //  mask off high bits and check if 0
+        //  mask off non high bits and check if 0
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)] // Needs a push
         private static bool CheckBytesInAsciiRange(long check)
@@ -444,5 +551,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
 
         private static bool CheckBytesInAsciiRange(sbyte check)
             => check > 0;
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)] // Needs a push
+        private static bool CheckBytesNotNull(Vector<byte> check)
+        {
+            // Vectorized byte range check, signed byte != null
+            return !Vector.EqualsAny(check, Vector<byte>.Zero);
+        }
+
+        // Validate: bytes != 0
+        //  Subtract 1 from all bytes to move 0 to high bits
+        //  bitwise and with ~check so high bits are only set for bytes that were originally 0
+        //  mask off non high bits and check if 0
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)] // Needs a push
+        private static bool CheckBytesNotNull(long check)
+        {
+            const long HighBits = unchecked((long)0x8080808080808080L);
+            return ((check - 0x0101010101010101L) & ~check & HighBits) == 0;
+        }
+
+        private static bool CheckBytesNotNull(int check)
+        {
+            const int HighBits = unchecked((int)0x80808080);
+            return ((check - 0x01010101) & ~check & HighBits) == 0;
+        }
+
+        private static bool CheckBytesNotNull(short check)
+        {
+            const short HighBits = unchecked((short)0x8080);
+            return ((check - 0x0101) & ~check & HighBits) == 0;
+        }
+
+        private static bool CheckBytesNotNull(sbyte check)
+            => check != 0;
     }
 }
diff --git a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
index 4522fedd62935959bc47654ff39cddeee4919d3f..5e202f3efa2945413bc950e219bbab93d2ac3aba 100644
--- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
+++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
@@ -222,6 +222,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel
             }
             _loaded = true;
 
+            Options.Latin1RequestHeaders = ConfigurationReader.Latin1RequestHeaders;
+
             LoadDefaultCert(ConfigurationReader);
 
             foreach (var endpoint in ConfigurationReader.Endpoints)
diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
index c383945c98b592d5ed985eb795fa724cff9868d2..35a5b0bd49a966002a6637752ea8a6093a5864e0 100644
--- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
+++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
@@ -92,6 +92,11 @@ 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.
diff --git a/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs b/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs
index a7603b9190fd9a4447406390aa3085834e6972f4..d87f1f814fea09e8f3ba0bc739efc10611cda3e7 100644
--- a/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs
+++ b/src/Servers/Kestrel/Core/test/HttpRequestHeadersTests.cs
@@ -318,6 +318,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         public void ValueReuseOnlyWhenAllowed(bool reuseValue, KnownHeader header)
         {
             const string HeaderValue = "Hello";
+
             var headers = new HttpRequestHeaders(reuseHeaderValues: reuseValue);
 
             for (var i = 0; i < 6; i++)
@@ -336,14 +337,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
                 Assert.Equal(values.PrevHeaderValue, values.NextHeaderValue);
                 if (reuseValue)
                 {
-                    // When materalized string is reused previous and new should be the same object
+                    // When materialized string is reused previous and new should be the same object
                     Assert.Same(values.PrevHeaderValue, values.NextHeaderValue);
                 }
                 else
                 {
-                    // When materalized string is not reused previous and new should be the different objects
+                    // When materialized string is not reused previous and new should be the different objects
                     Assert.NotSame(values.PrevHeaderValue, values.NextHeaderValue);
-            }
+                }
             }
         }
 
@@ -483,6 +484,89 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             }
         }
 
+        [Theory]
+        [MemberData(nameof(KnownRequestHeaders))]
+        public void Latin1ValuesAcceptedInLatin1ModeButNotReused(bool reuseValue, KnownHeader header)
+        {
+            var headers = new HttpRequestHeaders(reuseHeaderValues: reuseValue, useLatin1: true);
+
+            var headerValue = new char[127]; // 64 + 32 + 16 + 8 + 4 + 2 + 1
+            for (var i = 0; i < headerValue.Length; i++)
+            {
+                headerValue[i] = 'a';
+            }
+
+            for (var i = 0; i < headerValue.Length; i++)
+            {
+                // Set non-ascii Latin char that is valid Utf16 when widened; but not a valid utf8 -> utf16 conversion.
+                headerValue[i] = '\u00a3';
+
+                for (var mode = 0; mode <= 1; mode++)
+                {
+                    string headerValueUtf16Latin1CrossOver;
+                    if (mode == 0)
+                    {
+                        // Full length
+                        headerValueUtf16Latin1CrossOver = new string(headerValue);
+                    }
+                    else
+                    {
+                        // Truncated length (to ensure different paths from changing lengths in matching)
+                        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();
+
+                    Assert.False(latinValueSpan.SequenceEqual(Encoding.ASCII.GetBytes(headerValueUtf16Latin1CrossOver)));
+
+                    headers.Append(headerName, latinValueSpan);
+                    headers.OnHeadersComplete();
+                    var parsedHeaderValue = ((IHeaderDictionary)headers)[header.Name].ToString();
+
+                    Assert.Equal(headerValueUtf16Latin1CrossOver, parsedHeaderValue);
+                    Assert.NotSame(headerValueUtf16Latin1CrossOver, parsedHeaderValue);
+                }
+
+                // Reset back to Ascii
+                headerValue[i] = 'a';
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(KnownRequestHeaders))]
+        public void NullCharactersRejectedInUTF8AndLatin1Mode(bool useLatin1, KnownHeader header)
+        {
+            var headers = new HttpRequestHeaders(useLatin1: useLatin1);
+
+            var valueArray = new char[127]; // 64 + 32 + 16 + 8 + 4 + 2 + 1
+            for (var i = 0; i < valueArray.Length; i++)
+            {
+                valueArray[i] = 'a';
+            }
+
+            for (var i = 1; i < valueArray.Length; i++)
+            {
+                // Set non-ascii Latin char that is valid Utf16 when widened; but not a valid utf8 -> utf16 conversion.
+                valueArray[i] = '\0';
+                string valueString = new string(valueArray);
+
+                headers.Reset();
+
+                Assert.Throws<InvalidOperationException>(() =>
+                {
+                    var headerName = Encoding.ASCII.GetBytes(header.Name).AsSpan();
+                    var valueSpan = Encoding.ASCII.GetBytes(valueString).AsSpan();
+
+                    headers.Append(headerName, valueSpan);
+                });
+
+                valueArray[i] = 'a';
+            }
+        }
+
         [Fact]
         public void ValueReuseNeverWhenUnknownHeader()
         {
diff --git a/src/Servers/Kestrel/Core/test/UTF8Decoding.cs b/src/Servers/Kestrel/Core/test/UTF8Decoding.cs
index 532e13781d994da9562dfb6bcfc4c6d996936aed..4c4e37740047ea12a8f6994b45a7bff8961d7b4b 100644
--- a/src/Servers/Kestrel/Core/test/UTF8Decoding.cs
+++ b/src/Servers/Kestrel/Core/test/UTF8Decoding.cs
@@ -17,7 +17,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 = encodedBytes.AsSpan().GetAsciiOrUTF8StringNonNullCharacters();
+            var s = encodedBytes.AsSpan().GetRequestHeaderStringNonNullCharacters(useLatin1: false);
 
             Assert.Equal(1, s.Length);
         }
@@ -35,7 +35,7 @@ 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>(() => byteRange.AsSpan().GetAsciiOrUTF8StringNonNullCharacters());
+                    Assert.Throws<InvalidOperationException>(() => byteRange.AsSpan().GetRequestHeaderStringNonNullCharacters(useLatin1: false));
                 }
             }
         }
diff --git a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs
index 5a47957cdf2a15e3facc25ee4e899176ddcbc874..23fb6115518ecbcf61090d53dd2c4cadeba6c440 100644
--- a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs
+++ b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs
@@ -456,6 +456,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests
             Assert.True(ran3);
         }
 
+        [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);
+        }
+
         private static string GetCertificatePath()
         {
             var appData = Environment.GetEnvironmentVariable("APPDATA");
diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/BytesToStringBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/BytesToStringBenchmark.cs
index 28f365d7da45a191b95f894e63849656084906a3..cd6d19b75c9167e67f9f1e4d3ee5503cdf4d975f 100644
--- a/src/Servers/Kestrel/perf/Kestrel.Performance/BytesToStringBenchmark.cs
+++ b/src/Servers/Kestrel/perf/Kestrel.Performance/BytesToStringBenchmark.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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 BenchmarkDotNet.Attributes;
@@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
         {
             for (uint i = 0; i < Iterations; i++)
             {
-                HttpUtilities.GetAsciiOrUTF8StringNonNullCharacters(_utf8Bytes);
+                HttpUtilities.GetRequestHeaderStringNonNullCharacters(_utf8Bytes, useLatin1: false);
             }
         }
 
diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs
index e38dfb19a93166fada117fdebff90e36839ae4d4..b2f3292f34153aaf0e42dcf1047ce38802038002 100644
--- a/src/Servers/Kestrel/shared/KnownHeaders.cs
+++ b/src/Servers/Kestrel/shared/KnownHeaders.cs
@@ -985,7 +985,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.GetAsciiOrUTF8StringNonNullCharacters();
+                var valueStr = value.GetRequestHeaderStringNonNullCharacters(_useLatin1);
                 if ((_bits & flag) == 0)
                 {{
                     // We didn't already have a header set, so add a new one.
@@ -1003,7 +1003,7 @@ $@"        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.GetAsciiOrUTF8StringNonNullCharacters();
+                var valueStr = value.GetRequestHeaderStringNonNullCharacters(_useLatin1);
                 AppendUnknownHeaders(name, valueStr);
             }}
         }}" : "")}
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
index 4937000db9e187f3f16cdf2e8991fb1fcbc0cae7..4b1bced2303cde55baf35936fd285e6b21ae2573 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
@@ -4408,5 +4408,65 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             Assert.Single(_decodedHeaders);
             Assert.Equal("Custom Value", _decodedHeaders["CustomName"]);
         }
+
+        [Fact]
+        public async Task HEADERS_Received_Latin1_AcceptedWhenLatin1OptionIsConfigured()
+        {
+            _serviceContext.ServerOptions.Latin1RequestHeaders = true;
+
+            var headers = new[]
+            {
+                new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
+                new KeyValuePair<string, string>(HeaderNames.Path, "/"),
+                new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
+                // The HPackEncoder will encode £ as 0xA3 aka Latin1 encoding.
+                new KeyValuePair<string, string>("X-Test", "£"),
+            };
+
+            await InitializeConnectionAsync(context =>
+            {
+                Assert.Equal("£", context.Request.Headers["X-Test"]);
+                return Task.CompletedTask;
+            });
+
+            await StartStreamAsync(1, headers, endStream: true);
+
+            var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+                withLength: 55,
+                withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+                withStreamId: 1);
+
+            await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+            _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
+
+            Assert.Equal(3, _decodedHeaders.Count);
+            Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+            Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+            Assert.Equal("0", _decodedHeaders["content-length"]);
+        }
+
+        [Fact]
+        public async Task HEADERS_Received_Latin1_RejectedWhenLatin1OptionIsNotConfigured()
+        {
+            var headers = new[]
+            {
+                new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
+                new KeyValuePair<string, string>(HeaderNames.Path, "/"),
+                new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
+                // The HPackEncoder will encode £ as 0xA3 aka Latin1 encoding.
+                new KeyValuePair<string, string>("X-Test", "£"),
+            };
+
+            await InitializeConnectionAsync(_noopApplication);
+
+            await StartStreamAsync(1, headers, endStream: true);
+
+            await WaitForConnectionErrorAsync<Http2ConnectionErrorException>(
+                ignoreNonGoAwayFrames: true,
+                expectedLastStreamId: 1,
+                expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR,
+                expectedErrorMessage: CoreStrings.BadRequest_MalformedRequestInvalidHeaders);
+        }
     }
 }
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
index 5b875bffab164ee837edfd24b8474bb4cd10fa0c..d3d7e0e7ae45e935c8bb55c11b571ad6baf77e32 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
@@ -402,7 +402,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
 
         void IHttpHeadersHandler.OnHeader(Span<byte> name, Span<byte> value)
         {
-            _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
+            _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetRequestHeaderStringNonNullCharacters(useLatin1: _serviceContext.ServerOptions.Latin1RequestHeaders);
         }
 
         void IHttpHeadersHandler.OnHeadersComplete() { }
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs
index ed53a2fca8555573ec596a8a59915edae700c257..c10d9d08a077735df8e7a46929667d1d08760cf1 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs
@@ -1653,6 +1653,69 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
             }
         }
 
+        [Fact]
+        public async Task Latin1HeaderValueAcceptedWhenLatin1OptionIsConfigured()
+        {
+            var testContext = new TestServiceContext(LoggerFactory);
+
+            testContext.ServerOptions.Latin1RequestHeaders = true;
+
+            await using (var server = new TestServer(context =>
+            {
+                Assert.Equal("£", context.Request.Headers["X-Test"]);
+                return Task.CompletedTask;
+            }, testContext))
+            {
+                using (var connection = server.CreateConnection())
+                {
+                    // The StreamBackedTestConnection will encode £ using the "iso-8859-1" aka Latin1 encoding.
+                    // It will be encoded as 0xA3 which isn't valid UTF-8.
+                    await connection.Send(
+                        "GET / HTTP/1.1",
+                        "Host:",
+                        "X-Test: £",
+                        "",
+                        "");
+
+                    await connection.Receive(
+                        "HTTP/1.1 200 OK",
+                        $"Date: {testContext.DateHeaderValue}",
+                        "Content-Length: 0",
+                        "",
+                        "");
+                }
+            }
+        }
+
+        [Fact]
+        public async Task Latin1HeaderValueRejectedWhenLatin1OptionIsNotConfigured()
+        {
+            var testContext = new TestServiceContext(LoggerFactory);
+
+            await using (var server = new TestServer(_ => Task.CompletedTask, testContext))
+            {
+                using (var connection = server.CreateConnection())
+                {
+                    // The StreamBackedTestConnection will encode £ using the "iso-8859-1" aka Latin1 encoding.
+                    // It will be encoded as 0xA3 which isn't valid UTF-8.
+                    await connection.Send(
+                        "GET / HTTP/1.1",
+                        "Host:",
+                        "X-Test: £",
+                        "",
+                        "");
+
+                    await connection.ReceiveEnd(
+                        "HTTP/1.1 400 Bad Request",
+                        "Connection: close",
+                        $"Date: {testContext.DateHeaderValue}",
+                        "Content-Length: 0",
+                        "",
+                        "");
+                }
+            }
+        }
+
         public static TheoryData<string, string> HostHeaderData => HttpParsingData.HostHeaderData;
     }
 }