diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index c24c54defa81..472e34a318a1 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -713,4 +713,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l Responses with status code 205 cannot have a non-zero Content-Length value. + + A control stream used by the connection was closed or reset. + \ No newline at end of file diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs index 4993ea7f1264..c94ae4f4dbe7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -198,9 +198,49 @@ public void Tick(DateTimeOffset now) return; } + ValidateOpenControlStreams(now); UpdateStreamTimeouts(now); } + private void ValidateOpenControlStreams(DateTimeOffset now) + { + var ticks = now.Ticks; + + // This method validates that a connnection's control streams are open. + // + // They're checked on a delayed timer because when a connection is aborted or timed out, notifications are sent to open streams + // and the connection simultaneously. This is a problem because when a control stream is closed the connection should be aborted + // with the H3_CLOSED_CRITICAL_STREAM status. There is a race between the connection closing for the real reason, and control + // streams closing the connection with H3_CLOSED_CRITICAL_STREAM. + // + // Realistically, control streams are never closed except when the connection is. A small delay in aborting the connection in the + // unlikely situation where a control stream is incorrectly closed should be fine. + ValidateOpenControlStream(OutboundControlStream, this, ticks); + ValidateOpenControlStream(ControlStream, this, ticks); + ValidateOpenControlStream(EncoderStream, this, ticks); + ValidateOpenControlStream(DecoderStream, this, ticks); + + static void ValidateOpenControlStream(Http3ControlStream? stream, Http3Connection connection, long ticks) + { + if (stream != null) + { + if (stream.IsCompleted || stream.IsAborted || stream.EndStreamReceived) + { + // If a control stream is no longer active then set a timeout so that the connection is aborted next tick. + if (stream.StreamTimeoutTicks == default) + { + stream.StreamTimeoutTicks = ticks; + } + + if (stream.StreamTimeoutTicks < ticks) + { + connection.OnStreamConnectionError(new Http3ConnectionErrorException("A control stream used by the connection was closed or reset.", Http3ErrorCode.ClosedCriticalStream)); + } + } + } + } + } + private void UpdateStreamTimeouts(DateTimeOffset now) { // This method checks for timeouts: @@ -271,7 +311,7 @@ private void UpdateStreamTimeouts(DateTimeOffset now) { // Cancel connection to be consistent with other data rate limits. Log.ResponseMinimumDataRateNotSatisfied(_context.ConnectionId, stream.TraceIdentifier); - Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied), Http3ErrorCode.InternalError); + OnStreamConnectionError(new Http3ConnectionErrorException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied, Http3ErrorCode.InternalError)); } } } @@ -651,8 +691,7 @@ private async ValueTask ProcessOutboundControlStreamAsync(Http3ControlStream con { try { - await controlStream.SendStreamIdAsync(id: 0); - await controlStream.SendSettingsFrameAsync(); + await controlStream.ProcessOutboundSendsAsync(id: 0); } catch (Exception ex) { @@ -778,6 +817,11 @@ void IHttp3StreamLifetimeHandler.OnStreamCompleted(IHttp3Stream stream) } void IHttp3StreamLifetimeHandler.OnStreamConnectionError(Http3ConnectionErrorException ex) + { + OnStreamConnectionError(ex); + } + + private void OnStreamConnectionError(Http3ConnectionErrorException ex) { Log.Http3ConnectionError(ConnectionId, ex); Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs index adf2080c06b1..b4048ffb4ee6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs @@ -29,9 +29,14 @@ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem private volatile int _isClosed; private long _headerType; private int _gracefulCloseInitiator; - private bool _connectionClosed; + private readonly object _completionLock = new(); private bool _haveReceivedSettingsFrame; + private StreamCompletionFlags _completionState; + + public bool EndStreamReceived => (_completionState & StreamCompletionFlags.EndStreamReceived) == StreamCompletionFlags.EndStreamReceived; + public bool IsAborted => (_completionState & StreamCompletionFlags.Aborted) == StreamCompletionFlags.Aborted; + public bool IsCompleted => (_completionState & StreamCompletionFlags.Completed) == StreamCompletionFlags.Completed; public long StreamId => _streamIdFeature.StreamId; @@ -59,8 +64,7 @@ public Http3ControlStream(Http3StreamContext context, long? headerType) private void OnStreamClosed() { - Abort(new ConnectionAbortedException("HTTP_CLOSED_CRITICAL_STREAM"), Http3ErrorCode.InternalError); - _connectionClosed = true; + ApplyCompletionFlag(StreamCompletionFlags.Completed); } public PipeReader Input => _context.Transport.Input; @@ -74,15 +78,27 @@ private void OnStreamClosed() public void Abort(ConnectionAbortedException abortReason, Http3ErrorCode errorCode) { - // TODO - Should there be a check here to track abort state to avoid - // running twice for a request? + lock (_completionLock) + { + if (IsCompleted || IsAborted) + { + return; + } + + var (oldState, newState) = ApplyCompletionFlag(StreamCompletionFlags.Aborted); + + if (oldState == newState) + { + return; + } - Log.Http3StreamAbort(_context.ConnectionId, errorCode, abortReason); + Log.Http3StreamAbort(_context.ConnectionId, errorCode, abortReason); - _errorCodeFeature.Error = (long)errorCode; - _frameWriter.Abort(abortReason); + _errorCodeFeature.Error = (long)errorCode; + _frameWriter.Abort(abortReason); - Input.Complete(abortReason); + Input.Complete(abortReason); + } } public void OnInputOrOutputCompleted() @@ -101,9 +117,27 @@ private bool TryClose() return false; } - internal async ValueTask SendStreamIdAsync(long id) + private (StreamCompletionFlags OldState, StreamCompletionFlags NewState) ApplyCompletionFlag(StreamCompletionFlags completionState) + { + lock (_completionLock) + { + var oldCompletionState = _completionState; + _completionState |= completionState; + + return (oldCompletionState, _completionState); + } + } + + internal async ValueTask ProcessOutboundSendsAsync(long id) { + _streamClosedFeature.OnClosed(static state => + { + var stream = (Http3ControlStream)state!; + stream.OnStreamClosed(); + }, this); + await _frameWriter.WriteStreamIdAsync(id); + await _frameWriter.WriteSettingsAsync(_serverPeerSettings.GetNonProtocolDefaults()); } internal ValueTask SendGoAway(long id) @@ -112,11 +146,6 @@ internal ValueTask SendGoAway(long id) return _frameWriter.WriteGoAway(id); } - internal async ValueTask SendSettingsFrameAsync() - { - await _frameWriter.WriteSettingsAsync(_serverPeerSettings.GetNonProtocolDefaults()); - } - private async ValueTask TryReadStreamHeaderAsync() { // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2 @@ -212,6 +241,7 @@ public async Task ProcessRequestAsync(IHttpApplication appli } finally { + ApplyCompletionFlag(StreamCompletionFlags.Completed); _context.StreamLifetimeHandler.OnStreamCompleted(this); } } @@ -241,12 +271,6 @@ private async Task HandleControlStream() if (result.IsCompleted) { - if (!_connectionClosed) - { - // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2.1-2 - throw new Http3ConnectionErrorException(CoreStrings.Http3ErrorControlStreamClientClosedInbound, Http3ErrorCode.ClosedCriticalStream); - } - return; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 8b6ad2dfd54d..e867076bba22 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -62,8 +62,8 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS protected readonly Http3RawFrame _incomingFrame = new(); public bool EndStreamReceived => (_completionState & StreamCompletionFlags.EndStreamReceived) == StreamCompletionFlags.EndStreamReceived; - private bool IsAborted => (_completionState & StreamCompletionFlags.Aborted) == StreamCompletionFlags.Aborted; - private bool IsCompleted => (_completionState & StreamCompletionFlags.Completed) == StreamCompletionFlags.Completed; + public bool IsAborted => (_completionState & StreamCompletionFlags.Aborted) == StreamCompletionFlags.Aborted; + public bool IsCompleted => (_completionState & StreamCompletionFlags.Completed) == StreamCompletionFlags.Completed; public Pipe RequestBodyPipe { get; private set; } = default!; public long? InputRemaining { get; internal set; } @@ -1232,16 +1232,6 @@ private enum PseudoHeaderFields Unknown = 0x40000000 } - [Flags] - private enum StreamCompletionFlags - { - None = 0, - EndStreamReceived = 1, - AbortedRead = 2, - Aborted = 4, - Completed = 8, - } - private static class GracefulCloseInitiator { public const int None = 0; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs index 590755fda1a9..379b5e98444a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs @@ -36,6 +36,10 @@ internal interface IHttp3Stream bool IsRequestStream { get; } + bool EndStreamReceived { get; } + bool IsAborted { get; } + bool IsCompleted { get; } + string TraceIdentifier { get; } void Abort(ConnectionAbortedException abortReason, Http3ErrorCode errorCode); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/StreamCompletionFlags.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/StreamCompletionFlags.cs new file mode 100644 index 000000000000..78d1edb1f6b2 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/StreamCompletionFlags.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; + +[Flags] +internal enum StreamCompletionFlags +{ + None = 0, + EndStreamReceived = 1, + AbortedRead = 2, + Aborted = 4, + Completed = 8, +} diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs index 48d21d37ca88..d58061344f24 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs @@ -120,7 +120,7 @@ public static void StreamShutdownWrite(ILogger logger, QuicStreamContext streamC } } - [LoggerMessage(11, LogLevel.Debug, @"Stream id ""{ConnectionId}"" read aborted by peer with error code {ErrorCode}.", EventName = "StreamAborted", SkipEnabledCheck = true)] + [LoggerMessage(11, LogLevel.Debug, @"Stream id ""{ConnectionId}"" read aborted by peer with error code {ErrorCode}.", EventName = "StreamAbortedRead", SkipEnabledCheck = true)] private static partial void StreamAbortedReadCore(ILogger logger, string connectionId, long errorCode); public static void StreamAbortedRead(ILogger logger, QuicStreamContext streamContext, long errorCode) @@ -131,7 +131,7 @@ public static void StreamAbortedRead(ILogger logger, QuicStreamContext streamCon } } - [LoggerMessage(12, LogLevel.Debug, @"Stream id ""{ConnectionId}"" write aborted by peer with error code {ErrorCode}.", EventName = "StreamAborted", SkipEnabledCheck = true)] + [LoggerMessage(12, LogLevel.Debug, @"Stream id ""{ConnectionId}"" write aborted by peer with error code {ErrorCode}.", EventName = "StreamAbortedWrite", SkipEnabledCheck = true)] private static partial void StreamAbortedWriteCore(ILogger logger, string connectionId, long errorCode); public static void StreamAbortedWrite(ILogger logger, QuicStreamContext streamContext, long errorCode) @@ -210,6 +210,28 @@ public static void StreamReused(ILogger logger, QuicStreamContext streamContext) [LoggerMessage(21, LogLevel.Debug, "QUIC listener aborted.", EventName = "ConnectionListenerAborted")] public static partial void ConnectionListenerAborted(ILogger logger, Exception exception); + [LoggerMessage(22, LogLevel.Debug, @"Stream id ""{ConnectionId}"" read timed out.", EventName = "StreamTimeoutRead", SkipEnabledCheck = true)] + private static partial void StreamTimeoutReadCore(ILogger logger, string connectionId); + + public static void StreamTimeoutRead(ILogger logger, QuicStreamContext streamContext) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + StreamTimeoutReadCore(logger, streamContext.ConnectionId); + } + } + + [LoggerMessage(23, LogLevel.Debug, @"Stream id ""{ConnectionId}"" write timed out.", EventName = "StreamTimeoutWrite", SkipEnabledCheck = true)] + private static partial void StreamTimeoutWriteCore(ILogger logger, string connectionId); + + public static void StreamTimeoutWrite(ILogger logger, QuicStreamContext streamContext) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + StreamTimeoutWriteCore(logger, streamContext.ConnectionId); + } + } + private static StreamType GetStreamType(QuicStreamContext streamContext) => streamContext.CanRead && streamContext.CanWrite ? StreamType.Bidirectional diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs index b1776ad06aa2..9a95793a59de 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs @@ -270,7 +270,7 @@ private async ValueTask DoReceiveAsync() } } } - catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted) + catch (QuicException ex) when (ex.QuicError is QuicError.StreamAborted or QuicError.ConnectionAborted) { // Abort from peer. _error = ex.ApplicationErrorCode; @@ -281,18 +281,15 @@ private async ValueTask DoReceiveAsync() _clientAbort = true; } - catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted) + catch (QuicException ex) when (ex.QuicError is QuicError.ConnectionIdle) { - // Abort from peer. - _error = ex.ApplicationErrorCode; - QuicLog.StreamAbortedRead(_log, this, ex.ApplicationErrorCode.GetValueOrDefault()); + // Abort from timeout. + QuicLog.StreamTimeoutRead(_log, this); // This could be ignored if _shutdownReason is already set. error = new ConnectionResetException(ex.Message, ex); - - _clientAbort = true; } - catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted) + catch (QuicException ex) when (ex.QuicError is QuicError.OperationAborted) { // AbortRead has been called for the stream. error = new ConnectionAbortedException(ex.Message, ex); @@ -434,7 +431,7 @@ private async ValueTask DoSendAsync() } } } - catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted) + catch (QuicException ex) when (ex.QuicError is QuicError.StreamAborted or QuicError.ConnectionAborted) { // Abort from peer. _error = ex.ApplicationErrorCode; @@ -445,18 +442,15 @@ private async ValueTask DoSendAsync() _clientAbort = true; } - catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted) + catch (QuicException ex) when (ex.QuicError is QuicError.ConnectionIdle) { - // Abort from peer. - _error = ex.ApplicationErrorCode; - QuicLog.StreamAbortedWrite(_log, this, ex.ApplicationErrorCode.GetValueOrDefault()); + // Abort from timeout. + QuicLog.StreamTimeoutWrite(_log, this); // This could be ignored if _shutdownReason is already set. shutdownReason = new ConnectionResetException(ex.Message, ex); - - _clientAbort = true; } - catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted) + catch (QuicException ex) when (ex.QuicError is QuicError.OperationAborted) { // AbortWrite has been called for the stream. // Possibily might also get here from connection closing. diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index 443a1eb5e023..079ce4e2ec43 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -174,7 +174,7 @@ internal async Task WaitForGoAwayAsync(bool ignoreNonGoAwayFrames, long? expecte } } - internal void AssertConnectionError(Http3ErrorCode expectedErrorCode, Action matchExpectedErrorMessage = null, params string[] expectedErrorMessage) where TException : Exception + private void AssertConnectionError(Http3ErrorCode expectedErrorCode, Action matchExpectedErrorMessage = null, params string[] expectedErrorMessage) where TException : Exception { var currentError = (Http3ErrorCode)MultiplexedConnectionContext.Error; if (currentError != expectedErrorCode) @@ -1277,4 +1277,15 @@ public void OnClosed(Action callback, object state) } _onClosed.Add(new CloseAction(callback, state)); } + + public void Close() + { + if (_onClosed != null) + { + foreach (var onClose in _onClosed) + { + onClose.Callback(onClose.State); + } + } + } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs index 5373cb08b982..f557c0ea34e0 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs @@ -243,6 +243,7 @@ await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 0, expectedErrorCode: Http3ErrorCode.SettingsError, + matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ErrorControlStreamReservedSetting($"0x{settingIdentifier.ToString("X", CultureInfo.InvariantCulture)}")); } @@ -261,6 +262,7 @@ await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 0, expectedErrorCode: Http3ErrorCode.StreamCreationError, + matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams(name)); } @@ -281,44 +283,56 @@ await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 0, expectedErrorCode: Http3ErrorCode.UnexpectedFrame, + matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(Http3Formatting.ToFormattedType(f))); } [Fact] - public async Task ControlStream_ClientToServer_ClientCloses_ConnectionError() + public async Task ControlStream_ClientToServer_Completes_ConnectionError() { + var now = _serviceContext.MockSystemClock.UtcNow; + await Http3Api.InitializeConnectionAsync(_noopApplication); var controlStream = await Http3Api.CreateControlStream(id: 0); await controlStream.SendSettingsAsync(new List()); - await controlStream.EndStreamAsync(); + await controlStream.EndStreamAsync().DefaultTimeout(); + + // Wait for control stream to finish processing and exit. + await controlStream.OnStreamCompletedTask.DefaultTimeout(); + + Http3Api.TriggerTick(now); + Http3Api.TriggerTick(now + TimeSpan.FromSeconds(1)); await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 0, expectedErrorCode: Http3ErrorCode.ClosedCriticalStream, - expectedErrorMessage: CoreStrings.Http3ErrorControlStreamClientClosedInbound); + matchExpectedErrorMessage: AssertExpectedErrorMessages, + expectedErrorMessage: CoreStrings.Http3ErrorControlStreamClosed); } [Fact] - public async Task ControlStream_ServerToClient_ErrorInitializing_ConnectionError() + public async Task ControlStream_ServerToClient_Closes_ConnectionError() { - Http3Api.OnCreateServerControlStream = testStreamContext => - { - var controlStream = new Microsoft.AspNetCore.Testing.Http3ControlStream(Http3Api, testStreamContext); + var now = _serviceContext.MockSystemClock.UtcNow; - // Make server connection error when trying to write to control stream. - controlStream.StreamContext.Transport.Output.Complete(); + await Http3Api.InitializeConnectionAsync(_noopApplication); - return controlStream; - }; + var controlStream = await Http3Api.GetInboundControlStream(); - await Http3Api.InitializeConnectionAsync(_noopApplication); + controlStream.StreamContext.Close(); + + Http3Api.TriggerTick(now); + Http3Api.TriggerTick(now + TimeSpan.FromSeconds(1)); - Http3Api.AssertConnectionError( + await Http3Api.WaitForConnectionErrorAsync( + ignoreNonGoAwayFrames: true, + expectedLastStreamId: 0, expectedErrorCode: Http3ErrorCode.ClosedCriticalStream, - expectedErrorMessage: CoreStrings.Http3ControlStreamErrorInitializingOutbound); + matchExpectedErrorMessage: AssertExpectedErrorMessages, + expectedErrorMessage: CoreStrings.Http3ErrorControlStreamClosed); } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index da2092b13d6e..884d3d9a4f4a 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -2105,6 +2105,7 @@ await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 4, expectedErrorCode: Http3ErrorCode.UnexpectedFrame, + matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(Http3Formatting.ToFormattedType(f))); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs index 88782e9e57c8..835f5d65d509 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs @@ -306,10 +306,11 @@ public async Task ResponseDrain_SlowerThanMinimumDataRate_AbortsConnection() requestStream.StartStreamDisposeTcs.TrySetResult(); - await Http3Api.WaitForConnectionErrorAsync( + await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 4, Http3ErrorCode.InternalError, + matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied); Assert.Contains(TestSink.Writes, w => w.EventId.Name == "ResponseMinimumDataRateNotSatisfied");