Skip to content

Commit a717e7f

Browse files
authored
Check for sentinel value when setting HTTP/3 error code (#57976)
If no error code has been set, `IProtocolErrorFeature.Error` will be `-1`. If we pass that through verbatim, it will be caught by validation in the setter (ironically, of the same property on the same feature object), resulting in an exception and a Critical (but apparently benign) log message. Fixes #57933
1 parent 74bdb8b commit a717e7f

File tree

3 files changed

+87
-4
lines changed

3 files changed

+87
-4
lines changed

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ private void UpdateHighestOpenedRequestStreamId(long streamId)
101101
public string ConnectionId => _context.ConnectionId;
102102
public ITimeoutControl TimeoutControl => _context.TimeoutControl;
103103

104+
// The default error value is -1. If it hasn't been changed before abort is called then default to HTTP/3's NoError value.
105+
private Http3ErrorCode Http3ErrorCodeOrNoError => _errorCodeFeature.Error == -1 ? Http3ErrorCode.NoError : (Http3ErrorCode)_errorCodeFeature.Error;
106+
104107
public void StopProcessingNextRequest(ConnectionEndReason reason)
105108
=> StopProcessingNextRequest(serverInitiated: true, reason);
106109

@@ -505,12 +508,14 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
505508
}
506509
}
507510

511+
var errorCode = Http3ErrorCodeOrNoError;
512+
508513
// Abort active request streams.
509514
lock (_streams)
510515
{
511516
foreach (var stream in _streams.Values)
512517
{
513-
stream.Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error);
518+
stream.Abort(CreateConnectionAbortError(error, clientAbort), errorCode);
514519
}
515520
}
516521

@@ -546,7 +551,7 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
546551
}
547552

548553
// Complete
549-
Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error, reason);
554+
Abort(CreateConnectionAbortError(error, clientAbort), errorCode, reason);
550555

551556
// Wait for active requests to complete.
552557
while (_activeRequestCount > 0)
@@ -905,7 +910,7 @@ public void OnInputOrOutputCompleted()
905910
TryStopAcceptingStreams();
906911

907912
// Abort the connection using the error code the client used. For a graceful close, this should be H3_NO_ERROR.
908-
Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient), (Http3ErrorCode)_errorCodeFeature.Error, ConnectionEndReason.TransportCompleted);
913+
Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient), Http3ErrorCodeOrNoError, ConnectionEndReason.TransportCompleted);
909914
}
910915

911916
internal WebTransportSession OpenNewWebTransportSession(Http3Stream http3Stream)

src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ public void TriggerTick(TimeSpan timeSpan = default)
225225

226226
public async Task InitializeConnectionAsync(RequestDelegate application)
227227
{
228-
MultiplexedConnectionContext = new TestMultiplexedConnectionContext(this)
228+
MultiplexedConnectionContext ??= new TestMultiplexedConnectionContext(this)
229229
{
230230
ConnectionId = "TEST"
231231
};

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,84 @@ await Http3Api.InitializeConnectionAsync(async c =>
701701
await tcs.Task;
702702
}
703703

704+
[Fact]
705+
public async Task ErrorCodeIsValidOnConnectionTimeout()
706+
{
707+
// This test loosely repros the scenario in https://github.com/dotnet/aspnetcore/issues/57933.
708+
// In particular, there's a request from the server and, once a response has been sent,
709+
// the (simulated) transport throws a QuicException that surfaces through AcceptAsync.
710+
// This test confirms that Http3Connection.ProcessRequestsAsync doesn't (indirectly) cause
711+
// IProtocolErrorCodeFeature.Error to be set to (or left at) -1, which System.Net.Quic will
712+
// not accept.
713+
714+
// Used to signal that a request has been sent and a response has been received
715+
var requestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
716+
// Used to signal that the connection context has been aborted
717+
var abortTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
718+
719+
// InitializeConnectionAsync consumes the connection context, so set it first
720+
Http3Api.MultiplexedConnectionContext = new ThrowingMultiplexedConnectionContext(Http3Api, skipCount: 2, requestTcs, abortTcs);
721+
await Http3Api.InitializeConnectionAsync(_echoApplication);
722+
723+
await Http3Api.CreateControlStream();
724+
await Http3Api.GetInboundControlStream();
725+
var requestStream = await Http3Api.CreateRequestStream(Headers, endStream: true);
726+
var responseHeaders = await requestStream.ExpectHeadersAsync();
727+
728+
await requestStream.ExpectReceiveEndOfStream();
729+
await requestStream.OnDisposedTask.DefaultTimeout();
730+
731+
requestTcs.SetResult();
732+
733+
// By the time the connection context is aborted, the error code feature has been updated
734+
await abortTcs.Task.DefaultTimeout();
735+
736+
Http3Api.CloseServerGracefully();
737+
738+
var errorCodeFeature = Http3Api.MultiplexedConnectionContext.Features.Get<IProtocolErrorCodeFeature>();
739+
Assert.InRange(errorCodeFeature.Error, 0, (1L << 62) - 1); // Valid range for HTTP/3 error codes
740+
}
741+
742+
private sealed class ThrowingMultiplexedConnectionContext : TestMultiplexedConnectionContext
743+
{
744+
private int _skipCount;
745+
private readonly TaskCompletionSource _requestTcs;
746+
private readonly TaskCompletionSource _abortTcs;
747+
748+
/// <summary>
749+
/// After <paramref name="skipCount"/> calls to <see cref="AcceptAsync"/>, the next call will throw a <see cref="QuicException"/>
750+
/// (after waiting for <see cref="_requestTcs"/> to be set).
751+
///
752+
/// <paramref name="abortTcs"/> lets this type signal that <see cref="Abort"/> has been called.
753+
/// </summary>
754+
public ThrowingMultiplexedConnectionContext(Http3InMemory testBase, int skipCount, TaskCompletionSource requestTcs, TaskCompletionSource abortTcs)
755+
: base(testBase)
756+
{
757+
_skipCount = skipCount;
758+
_requestTcs = requestTcs;
759+
_abortTcs = abortTcs;
760+
}
761+
762+
public override async ValueTask<ConnectionContext> AcceptAsync(CancellationToken cancellationToken = default)
763+
{
764+
if (_skipCount-- <= 0)
765+
{
766+
await _requestTcs.Task.DefaultTimeout();
767+
throw new System.Net.Quic.QuicException(
768+
System.Net.Quic.QuicError.ConnectionTimeout,
769+
applicationErrorCode: null,
770+
"Connection timed out waiting for a response from the peer.");
771+
}
772+
return await base.AcceptAsync(cancellationToken);
773+
}
774+
775+
public override void Abort(ConnectionAbortedException abortReason)
776+
{
777+
_abortTcs.SetResult();
778+
base.Abort(abortReason);
779+
}
780+
}
781+
704782
private async Task<ConnectionContext> MakeRequestAsync(int index, KeyValuePair<string, string>[] headers, bool sendData, bool waitForServerDispose)
705783
{
706784
var requestStream = await Http3Api.CreateRequestStream(headers, endStream: !sendData);

0 commit comments

Comments
 (0)