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());