diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx
index 95dacfea4c3e..0951807eede5 100644
--- a/src/Servers/Kestrel/Core/src/CoreStrings.resx
+++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx
@@ -654,9 +654,30 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
     An error occurred after the response headers were sent, a reset is being sent.
   
   
-    The client sent a DATA frame before the HEADERS frame.
+    The client sent a DATA frame to a request stream before the HEADERS frame.
   
   
     The client sent a {frameType} frame after trailing HEADERS.
   
-
\ No newline at end of file
+  
+    The client sent a {frameType} frame to a request stream which isn't supported.
+  
+  
+    The client sent a {frameType} frame to the server which isn't supported.
+  
+  
+    The client sent a {frameType} frame to a control stream which isn't supported.
+  
+  
+    The client sent a SETTINGS frame to a control stream that already has settings.
+  
+  
+    The client sent a {frameType} frame to a control stream before the SETTINGS frame.
+  
+  
+    The client sent a reserved setting identifier: {identifier}
+  
+  
+    The client created multiple inbound {streamType} streams for the connection.
+  
+
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs
index f174f4b3266a..bdf9cacc1cd9 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs
@@ -1,6 +1,8 @@
 // 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 Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
+
 namespace System.Net.Http
 {
     internal partial class Http3RawFrame
@@ -9,9 +11,11 @@ internal partial class Http3RawFrame
 
         public Http3FrameType Type { get; internal set; }
 
+        public string FormattedType => Http3Formatting.ToFormattedType(Type);
+
         public override string ToString()
         {
-            return $"{Type} Length: {Length}";
+            return $"{FormattedType} Length: {Length}";
         }
     }
 }
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs
index 5088438ace97..343637bd41e1 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs
@@ -39,7 +39,7 @@ internal class Http3Connection : ITimeoutHandler
 
         private readonly Http3PeerSettings _serverSettings = new Http3PeerSettings();
         private readonly StreamCloseAwaitable _streamCompletionAwaitable = new StreamCloseAwaitable();
-
+        private readonly IProtocolErrorCodeFeature _errorCodeFeature;
 
         public Http3Connection(Http3ConnectionContext context)
         {
@@ -49,6 +49,8 @@ public Http3Connection(Http3ConnectionContext context)
             _timeoutControl = new TimeoutControl(this);
             _context.TimeoutControl ??= _timeoutControl;
 
+            _errorCodeFeature = context.ConnectionFeatures.Get()!;
+
             var httpLimits = context.ServiceContext.ServerOptions.Limits;
 
             _serverSettings.HeaderTableSize = (uint)httpLimits.Http3.HeaderTableSize;
@@ -76,6 +78,7 @@ internal long HighestStreamId
         public Http3ControlStream? ControlStream { get; set; }
         public Http3ControlStream? EncoderStream { get; set; }
         public Http3ControlStream? DecoderStream { get; set; }
+        public string ConnectionId => _context.ConnectionId;
 
         public async Task ProcessStreamsAsync(IHttpApplication httpApplication) where TContext : notnull
         {
@@ -163,7 +166,7 @@ private bool TryClose()
             return false;
         }
 
-        public void Abort(ConnectionAbortedException ex)
+        public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode)
         {
             bool previousState;
 
@@ -180,6 +183,7 @@ public void Abort(ConnectionAbortedException ex)
                     SendGoAway(_highestOpenedStreamId);
                 }
 
+                _errorCodeFeature.Error = (long)errorCode;
                 _multiplexedContext.Abort(ex);
             }
         }
@@ -326,9 +330,8 @@ internal async Task InnerProcessStreamsAsync(IHttpApplication(IHttpApplication 0)
@@ -364,7 +367,7 @@ internal async Task InnerProcessStreamsAsync(IHttpApplication()!;
+            _protocolErrorCodeFeature = context.ConnectionFeatures.Get()!;
 
             _frameWriter = new Http3FrameWriter(
                 context.Transport.Output,
@@ -144,43 +147,53 @@ private async ValueTask TryReadStreamIdAsync()
 
         public async Task ProcessRequestAsync(IHttpApplication application) where TContext : notnull
         {
-            var streamType = await TryReadStreamIdAsync();
-
-            if (streamType == -1)
+            try
             {
-                return;
-            }
+                var streamType = await TryReadStreamIdAsync();
 
-            if (streamType == ControlStream)
-            {
-                if (!_http3Connection.SetInboundControlStream(this))
+                if (streamType == -1)
                 {
-                    // TODO propagate these errors to connection.
-                    throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR");
+                    return;
                 }
 
-                await HandleControlStream();
-            }
-            else if (streamType == EncoderStream)
-            {
-                if (!_http3Connection.SetInboundEncoderStream(this))
+                if (streamType == ControlStream)
                 {
-                    throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR");
+                    if (!_http3Connection.SetInboundControlStream(this))
+                    {
+                        // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2.1
+                        throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("control"), Http3ErrorCode.StreamCreationError);
+                    }
+
+                    await HandleControlStream();
                 }
+                else if (streamType == EncoderStream)
+                {
+                    if (!_http3Connection.SetInboundEncoderStream(this))
+                    {
+                        // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2
+                        throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("encoder"), Http3ErrorCode.StreamCreationError);
+                    }
 
-                await HandleEncodingDecodingTask();
-            }
-            else if (streamType == DecoderStream)
-            {
-                if (!_http3Connection.SetInboundDecoderStream(this))
+                    await HandleEncodingDecodingTask();
+                }
+                else if (streamType == DecoderStream)
                 {
-                    throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR");
+                    if (!_http3Connection.SetInboundDecoderStream(this))
+                    {
+                        // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2
+                        throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("decoder"), Http3ErrorCode.StreamCreationError);
+                    }
+                    await HandleEncodingDecodingTask();
+                }
+                else
+                {
+                    // TODO Close the control stream as it's unexpected.
                 }
-                await HandleEncodingDecodingTask();
             }
-            else
+            catch (Http3ConnectionErrorException ex)
             {
-                // TODO Close the control stream as it's unexpected.
+                Log.Http3ConnectionError(_http3Connection.ConnectionId, ex);
+                _http3Connection.Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode);
             }
         }
 
@@ -212,8 +225,12 @@ private async Task HandleControlStream()
                         return;
                     }
                 }
-                catch (Http3StreamErrorException)
+                catch (Http3ConnectionErrorException ex)
                 {
+                    _protocolErrorCodeFeature.Error = (long)ex.ErrorCode;
+
+                    Log.Http3ConnectionError(_http3Connection.ConnectionId, ex);
+                    _http3Connection.Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode);
                 }
                 finally
                 {
@@ -238,26 +255,23 @@ private async ValueTask HandleEncodingDecodingTask()
 
         private ValueTask ProcessHttp3ControlStream(in ReadOnlySequence payload)
         {
-            // Two things:
-            // settings must be sent as the first frame of each control stream by each peer
-            // Can't send more than two settings frames.
             switch (_incomingFrame.Type)
             {
                 case Http3FrameType.Data:
                 case Http3FrameType.Headers:
-                case Http3FrameType.DuplicatePush:
                 case Http3FrameType.PushPromise:
-                    throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
+                    // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2
+                    throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame);
                 case Http3FrameType.Settings:
                     return ProcessSettingsFrameAsync(payload);
                 case Http3FrameType.GoAway:
-                    return ProcessGoAwayFrameAsync(payload);
+                    return ProcessGoAwayFrameAsync();
                 case Http3FrameType.CancelPush:
                     return ProcessCancelPushFrameAsync();
                 case Http3FrameType.MaxPushId:
                     return ProcessMaxPushIdFrameAsync();
                 default:
-                    return ProcessUnknownFrameAsync();
+                    return ProcessUnknownFrameAsync(_incomingFrame.Type);
             }
         }
 
@@ -265,7 +279,8 @@ private ValueTask ProcessSettingsFrameAsync(ReadOnlySequence payload)
         {
             if (_haveReceivedSettingsFrame)
             {
-                throw new Http3ConnectionException("H3_SETTINGS_ERROR");
+                // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-settings
+                throw new Http3ConnectionErrorException(CoreStrings.Http3ErrorControlStreamMultipleSettingsFrames, Http3ErrorCode.UnexpectedFrame);
             }
 
             _haveReceivedSettingsFrame = true;
@@ -299,10 +314,19 @@ private void ProcessSetting(long id, long value)
             // These are client settings, for outbound traffic.
             switch (id)
             {
+                case 0x0:
+                case 0x2:
+                case 0x3:
+                case 0x4:
+                case 0x5:
+                    // HTTP/2 settings are reserved.
+                    // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2.4.1-5
+                    var message = CoreStrings.FormatHttp3ErrorControlStreamReservedSetting("0x" + id.ToString("X", CultureInfo.InvariantCulture));
+                    throw new Http3ConnectionErrorException(message, Http3ErrorCode.SettingsError);
                 case (long)Http3SettingType.QPackMaxTableCapacity:
                     _http3Connection.ApplyMaxTableCapacity(value);
                     break;
-                case (long)Http3SettingType.MaxHeaderListSize:
+                case (long)Http3SettingType.MaxFieldSectionSize:
                     _http3Connection.ApplyMaxHeaderListSize(value);
                     break;
                 case (long)Http3SettingType.QPackBlockedStreams:
@@ -310,43 +334,58 @@ private void ProcessSetting(long id, long value)
                     break;
                 default:
                     // Ignore all unknown settings.
+                    // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2.4
                     break;
             }
         }
 
-        private ValueTask ProcessGoAwayFrameAsync(ReadOnlySequence payload)
+        private ValueTask ProcessGoAwayFrameAsync()
         {
-             throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
+            EnsureSettingsFrame(Http3FrameType.GoAway);
+
+            // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-goaway
+            // PUSH is not implemented so nothing to do.
+
+            // TODO: Double check the connection remains open.
+            return default;
         }
 
         private ValueTask ProcessCancelPushFrameAsync()
         {
-            if (!_haveReceivedSettingsFrame)
-            {
-                throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
-            }
+            EnsureSettingsFrame(Http3FrameType.CancelPush);
+
+            // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-cancel_push
+            // PUSH is not implemented so nothing to do.
 
             return default;
         }
 
         private ValueTask ProcessMaxPushIdFrameAsync()
         {
-            if (!_haveReceivedSettingsFrame)
-            {
-                throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
-            }
+            EnsureSettingsFrame(Http3FrameType.MaxPushId);
+
+            // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-cancel_push
+            // PUSH is not implemented so nothing to do.
 
             return default;
         }
 
-        private ValueTask ProcessUnknownFrameAsync()
+        private ValueTask ProcessUnknownFrameAsync(Http3FrameType frameType)
+        {
+            EnsureSettingsFrame(frameType);
+
+            // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-9
+            // Unknown frames must be explicitly ignored.
+            return default;
+        }
+
+        private void EnsureSettingsFrame(Http3FrameType frameType)
         {
             if (!_haveReceivedSettingsFrame)
             {
-                throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
+                var message = CoreStrings.FormatHttp3ErrorControlStreamFrameReceivedBeforeSettings(Http3Formatting.ToFormattedType(frameType));
+                throw new Http3ConnectionErrorException(message, Http3ErrorCode.MissingSettings);
             }
-
-            return default;
         }
 
         public void StopProcessingNextRequest()
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Formatting.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Formatting.cs
new file mode 100644
index 000000000000..91ac629805a7
--- /dev/null
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Formatting.cs
@@ -0,0 +1,49 @@
+// 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.Net.Http;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
+{
+    internal static class Http3Formatting
+    {
+        public static string ToFormattedType(Http3FrameType type)
+        {
+            return type switch
+            {
+                Http3FrameType.Data => "DATA",
+                Http3FrameType.Headers => "HEADERS",
+                Http3FrameType.CancelPush => "CANCEL_PUSH",
+                Http3FrameType.Settings => "SETTINGS",
+                Http3FrameType.PushPromise => "PUSH_PROMISE",
+                Http3FrameType.GoAway => "GO_AWAY",
+                Http3FrameType.MaxPushId => "MAX_PUSH_ID",
+                _ => type.ToString()
+            };
+        }
+
+        public static string ToFormattedErrorCode(Http3ErrorCode errorCode)
+        {
+            return errorCode switch
+            {
+                Http3ErrorCode.NoError => "H3_NO_ERROR",
+                Http3ErrorCode.ProtocolError => "H3_GENERAL_PROTOCOL_ERROR",
+                Http3ErrorCode.InternalError => "H3_INTERNAL_ERROR",
+                Http3ErrorCode.StreamCreationError => "H3_STREAM_CREATION_ERROR",
+                Http3ErrorCode.ClosedCriticalStream => "H3_CLOSED_CRITICAL_STREAM",
+                Http3ErrorCode.UnexpectedFrame => "H3_FRAME_UNEXPECTED",
+                Http3ErrorCode.FrameError => "H3_FRAME_ERROR",
+                Http3ErrorCode.ExcessiveLoad => "H3_EXCESSIVE_LOAD",
+                Http3ErrorCode.IdError => "H3_ID_ERROR",
+                Http3ErrorCode.SettingsError => "H3_SETTINGS_ERROR",
+                Http3ErrorCode.MissingSettings => "H3_MISSING_SETTINGS",
+                Http3ErrorCode.RequestRejected => "H3_REQUEST_REJECTED",
+                Http3ErrorCode.RequestCancelled => "H3_REQUEST_CANCELLED",
+                Http3ErrorCode.RequestIncomplete => "H3_REQUEST_INCOMPLETE",
+                Http3ErrorCode.ConnectError => "H3_CONNECT_ERROR",
+                Http3ErrorCode.VersionFallback => "H3_VERSION_FALLBACK",
+                _ => errorCode.ToString()
+            };
+        }
+    }
+}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs
index 2d6d5b53b279..32a2616c09ed 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs
@@ -28,7 +28,7 @@ internal List GetNonProtocolDefaults()
 
             if (MaxRequestHeaderFieldSize != DefaultMaxRequestHeaderFieldSize)
             {
-                list.Add(new Http3PeerSetting(Http3SettingType.MaxHeaderListSize, MaxRequestHeaderFieldSize));
+                list.Add(new Http3PeerSetting(Http3SettingType.MaxFieldSectionSize, MaxRequestHeaderFieldSize));
             }
 
             return list;
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs
index c90b2885c215..f7d971d2d0f0 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs
@@ -5,11 +5,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
 {
     internal enum Http3SettingType : long
     {
+        // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-5
         QPackMaxTableCapacity = 0x1,
         /// 
-        /// SETTINGS_MAX_HEADER_LIST_SIZE, default is unlimited.
+        /// SETTINGS_MAX_FIELD_SECTION_SIZE, default is unlimited.
+        /// https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-5
         /// 
-        MaxHeaderListSize = 0x6,
+        MaxFieldSectionSize = 0x6,
+        // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-5
         QPackBlockedStreams = 0x7
     }
 }
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs
index 35a0d2e1b383..90cc3b151e43 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.FeatureCollection.cs
@@ -58,7 +58,8 @@ IHeaderDictionary IHttpResponseTrailersFeature.Trailers
 
         void IHttpResetFeature.Reset(int errorCode)
         {
-            var abortReason = new ConnectionAbortedException(CoreStrings.FormatHttp3StreamResetByApplication((Http3ErrorCode)errorCode));
+            var message = CoreStrings.FormatHttp3StreamResetByApplication(Http3Formatting.ToFormattedErrorCode((Http3ErrorCode)errorCode));
+            var abortReason = new ConnectionAbortedException(message);
             ApplicationAbort(abortReason, (Http3ErrorCode)errorCode);
         }
     }
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
index 6e65e4b18999..fa269864302c 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
@@ -387,6 +387,16 @@ public async Task ProcessRequestAsync(IHttpApplication appli
                 error = ex;
                 Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode);
             }
+            catch (Http3ConnectionErrorException ex)
+            {
+                error = ex;
+                _errorCodeFeature.Error = (long)ex.ErrorCode;
+
+                Log.Http3ConnectionError(_http3Connection.ConnectionId, ex);
+                _http3Connection.Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode);
+
+                // TODO: HTTP/3 stream will be aborted by connection. Check this is correct.
+            }
             catch (Exception ex)
             {
                 error = ex;
@@ -446,14 +456,16 @@ private Task ProcessHttp3Stream(IHttpApplication application
                     return ProcessDataFrameAsync(payload);
                 case Http3FrameType.Headers:
                     return ProcessHeadersFrameAsync(application, payload);
-                // need to be on control stream
-                case Http3FrameType.DuplicatePush:
-                case Http3FrameType.PushPromise:
                 case Http3FrameType.Settings:
-                case Http3FrameType.GoAway:
                 case Http3FrameType.CancelPush:
+                case Http3FrameType.GoAway:
                 case Http3FrameType.MaxPushId:
-                    throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
+                    // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2.4
+                    // These frames need to be on a control stream
+                    throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame);
+                case Http3FrameType.PushPromise:
+                    // The server should never receive push promise
+                    throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame);
                 default:
                     return ProcessUnknownFrameAsync();
             }
@@ -461,6 +473,7 @@ private Task ProcessHttp3Stream(IHttpApplication application
 
         private Task ProcessUnknownFrameAsync()
         {
+            // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-9
             // Unknown frames must be explicitly ignored.
             return Task.CompletedTask;
         }
@@ -471,7 +484,7 @@ private Task ProcessHeadersFrameAsync(IHttpApplication appli
             // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1
             if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
             {
-                throw new Http3StreamErrorException(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3FrameType.Headers), Http3ErrorCode.UnexpectedFrame);
+                throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Headers)), Http3ErrorCode.UnexpectedFrame);
             }
 
             if (_requestHeaderParsingState == RequestHeaderParsingState.Headers)
@@ -514,14 +527,15 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload)
             // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1
             if (_requestHeaderParsingState == RequestHeaderParsingState.Ready)
             {
-                throw new Http3StreamErrorException(CoreStrings.Http3StreamErrorDataReceivedBeforeHeaders, Http3ErrorCode.UnexpectedFrame);
+                throw new Http3ConnectionErrorException(CoreStrings.Http3StreamErrorDataReceivedBeforeHeaders, Http3ErrorCode.UnexpectedFrame);
             }
 
             // DATA frame after trailing headers is invalid.
             // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1
             if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
             {
-                throw new Http3StreamErrorException(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3FrameType.Data), Http3ErrorCode.UnexpectedFrame);
+                var message = CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Data)));
+                throw new Http3ConnectionErrorException(message, Http3ErrorCode.UnexpectedFrame);
             }
 
             if (InputRemaining.HasValue)
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs
index 8edccac290bb..6c40b315b39d 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs
@@ -6,7 +6,7 @@
 
 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
 {
-    class Http3StreamErrorException : Exception
+    internal class Http3StreamErrorException : Exception
     {
         public Http3StreamErrorException(string message, Http3ErrorCode errorCode)
             : base($"HTTP/3 stream error ({errorCode}): {message}")
diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs
index 7b0829ab9552..34356e071370 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs
@@ -81,7 +81,7 @@ internal interface IKestrelTrace : ILogger
 
         void InvalidResponseHeaderRemoved();
 
-        void Http3ConnectionError(string connectionId, Http3ConnectionException ex);
+        void Http3ConnectionError(string connectionId, Http3ConnectionErrorException ex);
 
         void Http3ConnectionClosing(string connectionId);
 
diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs
index 8260d6da80f7..82182456580a 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs
@@ -134,16 +134,16 @@ internal class KestrelTrace : IKestrelTrace
             LoggerMessage.Define(LogLevel.Debug, new EventId(44, "Http3ConnectionClosed"),
                 @"Connection id ""{ConnectionId}"" is closed. The last processed stream ID was {HighestOpenedStreamId}.");
 
-        private static readonly Action _http3StreamAbort =
-            LoggerMessage.Define(LogLevel.Debug, new EventId(45, "Http3StreamAbort"),
+        private static readonly Action _http3StreamAbort =
+            LoggerMessage.Define(LogLevel.Debug, new EventId(45, "Http3StreamAbort"),
                 @"Trace id ""{TraceIdentifier}"": HTTP/3 stream error ""{error}"". An abort is being sent to the stream.");
 
-        private static readonly Action _http3FrameReceived =
-            LoggerMessage.Define(LogLevel.Trace, new EventId(46, "Http3FrameReceived"),
+        private static readonly Action _http3FrameReceived =
+            LoggerMessage.Define(LogLevel.Trace, new EventId(46, "Http3FrameReceived"),
                 @"Connection id ""{ConnectionId}"" received {type} frame for stream ID {id} with length {length}.");
 
-        private static readonly Action _http3FrameSending =
-            LoggerMessage.Define(LogLevel.Trace, new EventId(47, "Http3FrameSending"),
+        private static readonly Action _http3FrameSending =
+            LoggerMessage.Define(LogLevel.Trace, new EventId(47, "Http3FrameSending"),
                 @"Connection id ""{ConnectionId}"" sending {type} frame for stream ID {id} with length {length}.");
 
         protected readonly ILogger _logger;
@@ -329,7 +329,7 @@ public void InvalidResponseHeaderRemoved()
             _invalidResponseHeaderRemoved(_logger, null);
         }
 
-        public void Http3ConnectionError(string connectionId, Http3ConnectionException ex)
+        public void Http3ConnectionError(string connectionId, Http3ConnectionErrorException ex)
         {
             _http3ConnectionError(_logger, connectionId, ex);
         }
@@ -346,17 +346,26 @@ public void Http3ConnectionClosed(string connectionId, long highestOpenedStreamI
 
         public void Http3StreamAbort(string traceIdentifier, Http3ErrorCode error, ConnectionAbortedException abortReason)
         {
-            _http3StreamAbort(_logger, traceIdentifier, error, abortReason);
+            if (_logger.IsEnabled(LogLevel.Debug))
+            {
+                _http3StreamAbort(_logger, traceIdentifier, Http3Formatting.ToFormattedErrorCode(error), abortReason);
+            }
         }
 
         public void Http3FrameReceived(string connectionId, long streamId, Http3RawFrame frame)
         {
-            _http3FrameReceived(_logger, connectionId, frame.Type, streamId, frame.Length, null);
+            if (_logger.IsEnabled(LogLevel.Trace))
+            {
+                _http3FrameReceived(_logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.Length, null);
+            }
         }
 
         public void Http3FrameSending(string connectionId, long streamId, Http3RawFrame frame)
         {
-            _http3FrameSending(_logger, connectionId, frame.Type, streamId, frame.Length, null);
+            if (_logger.IsEnabled(LogLevel.Trace))
+            {
+                _http3FrameSending(_logger, connectionId, Http3Formatting.ToFormattedType(frame.Type), streamId, frame.Length, null);
+            }
         }
 
         public virtual void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs
index 887980770300..2528dd7b6fc7 100644
--- a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs
+++ b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs
@@ -60,7 +60,7 @@ public void Http2FrameReceived(string connectionId, Http2Frame frame) { }
         public void Http2FrameSending(string connectionId, Http2Frame frame) { }
         public void Http2MaxConcurrentStreamsReached(string connectionId) { }
         public void InvalidResponseHeaderRemoved() { }
-        public void Http3ConnectionError(string connectionId, Http3ConnectionException ex) { }
+        public void Http3ConnectionError(string connectionId, Http3ConnectionErrorException ex) { }
         public void Http3ConnectionClosing(string connectionId) { }
         public void Http3ConnectionClosed(string connectionId, long highestOpenedStreamId) { }
         public void Http3StreamAbort(string traceIdentifier, Http3ErrorCode error, ConnectionAbortedException abortReason) { }
diff --git a/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs b/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs
index 3f5271b4ec0c..ef99df0548c6 100644
--- a/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs
+++ b/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs
@@ -245,7 +245,7 @@ public void InvalidResponseHeaderRemoved()
             _trace2.InvalidResponseHeaderRemoved();
         }
 
-        public void Http3ConnectionError(string connectionId, Http3ConnectionException ex)
+        public void Http3ConnectionError(string connectionId, Http3ConnectionErrorException ex)
         {
             _trace1.Http3ConnectionError(connectionId, ex);
             _trace2.Http3ConnectionError(connectionId, ex);
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs
index ef03fbb55958..19bf3cb2e936 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs
@@ -3,15 +3,12 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
+using System.Globalization;
 using System.Net.Http;
 using System.Text;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Connections;
-using Microsoft.AspNetCore.Connections.Features;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Features;
-using Microsoft.AspNetCore.Testing;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
 using Microsoft.Net.Http.Headers;
 using Xunit;
 
@@ -19,19 +16,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
 {
     public class Http3ConnectionTests : Http3TestBase
     {
-        [Fact]
-        public async Task GoAwayReceived()
-        {
-            await InitializeConnectionAsync(_echoApplication);
-
-            var outboundcontrolStream = await CreateControlStream();
-            var inboundControlStream = await GetInboundControlStream();
-
-            Connection.Abort(new ConnectionAbortedException());
-            await _closedStateReached.Task.DefaultTimeout();
-            await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: true, expectedLastStreamId: 0, expectedErrorCode: 0);
-        }
-
         [Fact]
         public async Task CreateRequestStream_RequestCompleted_Disposed()
         {
@@ -83,5 +67,69 @@ public async Task GracefulServerShutdownSendsGoawayClosesConnection()
             MultiplexedConnectionContext.ConnectionClosingCts.Cancel();
             Assert.Null(await MultiplexedConnectionContext.AcceptAsync().DefaultTimeout());
         }
+
+        [Theory]
+        [InlineData(0x0)]
+        [InlineData(0x2)]
+        [InlineData(0x3)]
+        [InlineData(0x4)]
+        [InlineData(0x5)]
+        public async Task SETTINGS_ReservedSettingSent_ConnectionError(long settingIdentifier)
+        {
+            await InitializeConnectionAsync(_echoApplication);
+
+            var outboundcontrolStream = await CreateControlStream();
+            await outboundcontrolStream.SendSettingsAsync(new List
+            {
+                new Http3PeerSetting((Internal.Http3.Http3SettingType) settingIdentifier, 0) // reserved value
+            });
+
+            await GetInboundControlStream();
+
+            await WaitForConnectionErrorAsync(
+                ignoreNonGoAwayFrames: true,
+                expectedLastStreamId: 0,
+                expectedErrorCode: Http3ErrorCode.SettingsError,
+                expectedErrorMessage: CoreStrings.FormatHttp3ErrorControlStreamReservedSetting($"0x{settingIdentifier.ToString("X", CultureInfo.InvariantCulture)}"));
+        }
+
+        [Theory]
+        [InlineData(0, "control")]
+        [InlineData(2, "encoder")]
+        [InlineData(3, "decoder")]
+        public async Task InboundStreams_CreateMultiple_ConnectionError(int streamId, string name)
+        {
+            await InitializeConnectionAsync(_noopApplication);
+
+            await CreateControlStream(streamId);
+            await CreateControlStream(streamId);
+
+            await WaitForConnectionErrorAsync(
+                ignoreNonGoAwayFrames: true,
+                expectedLastStreamId: 0,
+                expectedErrorCode: Http3ErrorCode.StreamCreationError,
+                expectedErrorMessage: CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams(name));
+        }
+
+        [Theory]
+        [InlineData(nameof(Http3FrameType.Data))]
+        [InlineData(nameof(Http3FrameType.Headers))]
+        [InlineData(nameof(Http3FrameType.PushPromise))]
+        public async Task ControlStream_UnexpectedFrameType_ConnectionError(string frameType)
+        {
+            await InitializeConnectionAsync(_noopApplication);
+
+            var controlStream = await CreateControlStream();
+
+            var frame = new Http3RawFrame();
+            frame.Type = Enum.Parse(frameType);
+            await controlStream.SendFrameAsync(frame, Memory.Empty);
+
+            await WaitForConnectionErrorAsync(
+                ignoreNonGoAwayFrames: true,
+                expectedLastStreamId: 0,
+                expectedErrorCode: Http3ErrorCode.UnexpectedFrame,
+                expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(Http3Formatting.ToFormattedType(frame.Type)));
+        }
     }
 }
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
index b05114e7d204..a7fce40ae41f 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
@@ -11,6 +11,7 @@
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Internal;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
 using Microsoft.AspNetCore.Testing;
 using Microsoft.Net.Http.Headers;
 using Xunit;
@@ -760,7 +761,9 @@ public async Task ResetStream_ReturnStreamError()
 
             var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true);
 
-            await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.RequestCancelled, CoreStrings.FormatHttp3StreamResetByApplication(Http3ErrorCode.RequestCancelled));
+            await requestStream.WaitForStreamErrorAsync(
+                Http3ErrorCode.RequestCancelled,
+                CoreStrings.FormatHttp3StreamResetByApplication(Http3Formatting.ToFormattedErrorCode(Http3ErrorCode.RequestCancelled)));
         }
 
         [Fact]
@@ -1541,7 +1544,9 @@ public async Task ResetAfterCompleteAsync_GETWithResponseBodyAndTrailers_ResetsA
             var decodedTrailers = await requestStream.ExpectHeadersAsync();
             Assert.Equal("Custom Value", decodedTrailers["CustomName"]);
 
-            await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.NoError, expectedErrorMessage: "The HTTP/3 stream was reset by the application with error code NoError.");
+            await requestStream.WaitForStreamErrorAsync(
+                Http3ErrorCode.NoError,
+                expectedErrorMessage: "The HTTP/3 stream was reset by the application with error code H3_NO_ERROR.");
 
             clientTcs.SetResult(0);
             await appTcs.Task;
@@ -1609,7 +1614,9 @@ public async Task ResetAfterCompleteAsync_POSTWithResponseBodyAndTrailers_Reques
             var decodedTrailers = await requestStream.ExpectHeadersAsync();
             Assert.Equal("Custom Value", decodedTrailers["CustomName"]);
 
-            await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.NoError, expectedErrorMessage: "The HTTP/3 stream was reset by the application with error code NoError.");
+            await requestStream.WaitForStreamErrorAsync(
+                Http3ErrorCode.NoError,
+                expectedErrorMessage: "The HTTP/3 stream was reset by the application with error code H3_NO_ERROR.");
 
             clientTcs.SetResult(0);
             await appTcs.Task;
@@ -1622,7 +1629,9 @@ public async Task DataBeforeHeaders_UnexpectedFrameError()
 
             await requestStream.SendDataAsync(Encoding.UTF8.GetBytes("This is invalid."));
 
-            await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.UnexpectedFrame, expectedErrorMessage: "The client sent a DATA frame before the HEADERS frame.");
+            await requestStream.WaitForStreamErrorAsync(
+                Http3ErrorCode.UnexpectedFrame,
+                expectedErrorMessage: CoreStrings.Http3StreamErrorDataReceivedBeforeHeaders);
         }
 
         [Fact]
@@ -1680,7 +1689,9 @@ public async Task FrameAfterTrailers_UnexpectedFrameError()
             await requestStream.SendHeadersAsync(trailers, endStream: false);
             await requestStream.SendDataAsync(Encoding.UTF8.GetBytes("This is invalid."));
 
-            await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.UnexpectedFrame, expectedErrorMessage: "The client sent a Data frame after trailing HEADERS.");
+            await requestStream.WaitForStreamErrorAsync(
+                Http3ErrorCode.UnexpectedFrame,
+                expectedErrorMessage: CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Data)));
         }
 
         [Fact]
@@ -1727,5 +1738,44 @@ public async Task TrailersWithoutEndingStream_ErrorAccessingTrailers()
             var ex = await Assert.ThrowsAsync(() => readTrailersTcs.Task).DefaultTimeout();
             Assert.Equal("The request trailers are not available yet. They may not be available until the full request body is read.", ex.Message);
         }
+
+        [Theory]
+        [InlineData(nameof(Http3FrameType.MaxPushId))]
+        [InlineData(nameof(Http3FrameType.Settings))]
+        [InlineData(nameof(Http3FrameType.CancelPush))]
+        [InlineData(nameof(Http3FrameType.GoAway))]
+        public async Task UnexpectedRequestFrame(string frameType)
+        {
+            var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication);
+
+            var frame = new Http3RawFrame();
+            frame.Type = Enum.Parse(frameType);
+            await requestStream.SendFrameAsync(frame, Memory.Empty);
+
+            await requestStream.WaitForStreamErrorAsync(
+                Http3ErrorCode.UnexpectedFrame,
+                expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(frame.FormattedType));
+
+            await WaitForConnectionErrorAsync(
+                ignoreNonGoAwayFrames: true,
+                expectedLastStreamId: 0,
+                expectedErrorCode: Http3ErrorCode.UnexpectedFrame,
+                expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(frame.FormattedType));
+        }
+
+        [Theory]
+        [InlineData(nameof(Http3FrameType.PushPromise))]
+        public async Task UnexpectedServerFrame(string frameType)
+        {
+            var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication);
+
+            var frame = new Http3RawFrame();
+            frame.Type = Enum.Parse(frameType);
+            await requestStream.SendFrameAsync(frame, Memory.Empty);
+
+            await requestStream.WaitForStreamErrorAsync(
+                Http3ErrorCode.UnexpectedFrame,
+                expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(frame.FormattedType));
+        }
     }
 }
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
index ea4338926c3f..44ce3ac90d39 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
@@ -132,12 +132,13 @@ internal async ValueTask GetInboundControlStream()
                         return _inboundControlStream;
                     }
                 }
-            }    
-            
+            }
+
             return null;
         }
 
-        internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, long expectedLastStreamId, Http3ErrorCode expectedErrorCode)
+        internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, long expectedLastStreamId, Http3ErrorCode expectedErrorCode, params string[] expectedErrorMessage)
+            where TException : Exception
         {
             var frame = await _inboundControlStream.ReceiveFrameAsync();
 
@@ -149,10 +150,19 @@ internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, long
                 }
             }
 
-            VerifyGoAway(frame, expectedLastStreamId, expectedErrorCode);
+            VerifyGoAway(frame, expectedLastStreamId);
+
+            Assert.Equal((Http3ErrorCode)expectedErrorCode, (Http3ErrorCode)MultiplexedConnectionContext.Error);
+
+            if (expectedErrorMessage?.Length > 0)
+            {
+                var message = Assert.Single(LogMessages, m => m.Exception is TException);
+
+                Assert.Contains(expectedErrorMessage, expected => message.Exception.Message.Contains(expected));
+            }
         }
 
-        internal void VerifyGoAway(Http3FrameWithPayload frame, long expectedLastStreamId, Http3ErrorCode expectedErrorCode)
+        internal void VerifyGoAway(Http3FrameWithPayload frame, long expectedLastStreamId)
         {
             Assert.Equal(Http3FrameType.GoAway, frame.Type);
             var payload = frame.Payload;
@@ -170,6 +180,8 @@ protected async Task InitializeConnectionAsync(RequestDelegate application)
             // Skip all heartbeat and lifetime notification feature registrations.
             _connectionTask = Connection.ProcessStreamsAsync(new DummyApplication(application));
 
+            await GetInboundControlStream();
+
             await Task.CompletedTask;
         }
 
@@ -329,6 +341,19 @@ internal async Task ReceiveFrameAsync()
                     }
                 }
             }
+
+            internal async Task SendFrameAsync(Http3RawFrame frame, Memory data, bool endStream = false)
+            {
+                var outputWriter = _pair.Application.Output;
+                frame.Length = data.Length;
+                Http3FrameWriter.WriteHeader(frame, outputWriter);
+                await SendAsync(data.Span);
+
+                if (endStream)
+                {
+                    await _pair.Application.Output.CompleteAsync();
+                }
+            }
         }
 
         internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler, IProtocolErrorCodeFeature
@@ -364,37 +389,21 @@ public Http3RequestStream(Http3TestBase testBase, Http3Connection connection)
 
             public async Task SendHeadersAsync(IEnumerable> headers, bool endStream = false)
             {
-                var outputWriter = _pair.Application.Output;
                 var frame = new Http3RawFrame();
                 frame.PrepareHeaders();
                 var buffer = _headerEncodingBuffer.AsMemory();
                 var done = _qpackEncoder.BeginEncode(headers, buffer.Span, out var length);
-                frame.Length = length;
-                // TODO may want to modify behavior of input frames to mock different client behavior (client can send anything).
-                Http3FrameWriter.WriteHeader(frame, outputWriter);
-                await SendAsync(buffer.Span.Slice(0, length));
 
-                if (endStream)
-                {
-                    await _pair.Application.Output.CompleteAsync();
-                }
+                await SendFrameAsync(frame, buffer.Slice(0, length), endStream);
 
                 return done;
             }
 
             internal async Task SendDataAsync(Memory data, bool endStream = false)
             {
-                var outputWriter = _pair.Application.Output;
                 var frame = new Http3RawFrame();
                 frame.PrepareData();
-                frame.Length = data.Length;
-                Http3FrameWriter.WriteHeader(frame, outputWriter);
-                await SendAsync(data.Span);
-
-                if (endStream)
-                {
-                    await _pair.Application.Output.CompleteAsync();
-                }
+                await SendFrameAsync(frame, data, endStream);
             }
 
             internal async Task> ExpectHeadersAsync()
@@ -446,7 +455,7 @@ internal async Task WaitForStreamErrorAsync(Http3ErrorCode protocolError, string
                 _testBase.Logger.LogTrace("Input is completed");
 
                 Assert.True(readResult.IsCompleted);
-                Assert.Equal((long)protocolError, Error);
+                Assert.Equal(protocolError, (Http3ErrorCode)Error);
 
                 if (expectedErrorMessage != null)
                 {
@@ -484,7 +493,7 @@ public Http3ControlStream(Http3TestBase testBase)
                 var inputPipeOptions = GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool);
                 var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool);
                 _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
-                StreamContext = new TestStreamContext(canRead: false, canWrite: true, _pair, this);
+                StreamContext = new TestStreamContext(canRead: true, canWrite: false, _pair, this);
             }
 
             public Http3ControlStream(ConnectionContext streamContext)
@@ -508,6 +517,41 @@ void WriteSpan(PipeWriter pw)
                 await FlushAsync(writableBuffer);
             }
 
+            internal async Task SendSettingsAsync(List settings, bool endStream = false)
+            {
+                var frame = new Http3RawFrame();
+                frame.PrepareSettings();
+
+                var settingsLength = CalculateSettingsSize(settings);
+                var buffer = new byte[settingsLength];
+                WriteSettings(settings, buffer);
+
+                await SendFrameAsync(frame, buffer, endStream);
+            }
+
+            internal static int CalculateSettingsSize(List settings)
+            {
+                var length = 0;
+                foreach (var setting in settings)
+                {
+                    length += VariableLengthIntegerHelper.GetByteCount((long)setting.Parameter);
+                    length += VariableLengthIntegerHelper.GetByteCount(setting.Value);
+                }
+                return length;
+            }
+
+            internal static void WriteSettings(List settings, Span destination)
+            {
+                foreach (var setting in settings)
+                {
+                    var parameterLength = VariableLengthIntegerHelper.WriteInteger(destination, (long)setting.Parameter);
+                    destination = destination.Slice(parameterLength);
+
+                    var valueLength = VariableLengthIntegerHelper.WriteInteger(destination, (long)setting.Value);
+                    destination = destination.Slice(valueLength);
+                }
+            }
+
             public async ValueTask TryReadStreamIdAsync()
             {
                 while (true)
@@ -541,7 +585,7 @@ public async ValueTask TryReadStreamIdAsync()
             }
         }
 
-        public class TestMultiplexedConnectionContext : MultiplexedConnectionContext, IConnectionLifetimeNotificationFeature, IConnectionLifetimeFeature, IConnectionHeartbeatFeature
+        public class TestMultiplexedConnectionContext : MultiplexedConnectionContext, IConnectionLifetimeNotificationFeature, IConnectionLifetimeFeature, IConnectionHeartbeatFeature, IProtocolErrorCodeFeature
         {
             public readonly Channel ToServerAcceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions
             {
@@ -563,6 +607,7 @@ public TestMultiplexedConnectionContext(Http3TestBase testBase)
                 Features = new FeatureCollection();
                 Features.Set(this);
                 Features.Set(this);
+                Features.Set(this);
                 ConnectionClosedRequested = ConnectionClosingCts.Token;
             }
 
@@ -576,6 +621,8 @@ public TestMultiplexedConnectionContext(Http3TestBase testBase)
 
             public CancellationTokenSource ConnectionClosingCts { get; set; } = new CancellationTokenSource();
 
+            public long Error { get; set; }
+
             public override void Abort()
             {
                 Abort(new ConnectionAbortedException());