@@ -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