diff --git a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/Frames/Http3ErrorCode.cs b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/Frames/Http3ErrorCode.cs index fdfe04cee1580b..ebaa96f12cdc46 100644 --- a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/Frames/Http3ErrorCode.cs +++ b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http3/Frames/Http3ErrorCode.cs @@ -91,5 +91,20 @@ internal enum Http3ErrorCode : long /// The requested operation cannot be served over HTTP/3. The peer should retry over HTTP/1.1. /// VersionFallback = 0x110, + /// + /// H3_QPACK_DECOMPRESSION_FAILED (0x200): + /// The decoder failed to interpret an encoded field section and is not able to continue decoding that field section. + /// + QPackDecompressionFailed = 0x200, + /// + /// H3_QPACK_ENCODER_STREAM_ERROR (0x201): + /// The decoder failed to interpret an encoder instruction received on the encoder stream. + /// + QPackEncoderStreamError = 0x201, + /// + /// H3_QPACK_DECODER_STREAM_ERROR (0x202): + /// The encoder failed to interpret an decoder instruction received on the decoder stream. + /// + QPackDecoderStreamError = 0x202, } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs index 64ba4089d5dbca..ef2532b2b22d08 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs @@ -277,8 +277,12 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s } catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted && _connection.AbortException != null) { - // we close the connection, propagate the AbortException - throw new HttpRequestException(HttpRequestError.Unknown, SR.net_http_client_execution_error, _connection.AbortException); + // we closed the connection already, propagate the AbortException + HttpRequestError httpRequestError = _connection.AbortException is HttpProtocolException + ? HttpRequestError.HttpProtocolError + : HttpRequestError.Unknown; + + throw new HttpRequestException(httpRequestError, SR.net_http_client_execution_error, _connection.AbortException); } // It is possible for user's Content code to throw an unexpected OperationCanceledException. catch (OperationCanceledException ex) when (ex.CancellationToken == _requestBodyCancellationSource.Token || ex.CancellationToken == cancellationToken) @@ -300,6 +304,16 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s _connection.Abort(ex); throw new HttpRequestException(ex.HttpRequestError, SR.net_http_client_execution_error, ex); } + catch (QPackDecodingException ex) + { + Exception abortException = _connection.Abort(HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.QPackDecompressionFailed)); + throw new HttpRequestException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response, ex); + } + catch (QPackEncodingException ex) + { + _stream.Abort(QuicAbortDirection.Write, (long)Http3ErrorCode.InternalError); + throw new HttpRequestException(HttpRequestError.Unknown, SR.net_http_client_execution_error, ex); + } catch (Exception ex) { _stream.Abort(QuicAbortDirection.Write, (long)Http3ErrorCode.InternalError); @@ -307,6 +321,9 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s { throw; } + + // all exceptions should be already handled above + Debug.Fail($"Unexpected exception type in Http3RequestStream.SendAsync: {ex}"); throw new HttpRequestException(HttpRequestError.Unknown, SR.net_http_client_execution_error, ex); } finally @@ -1253,6 +1270,13 @@ private void HandleReadResponseContentException(Exception ex, CancellationToken _connection.Abort(exception); throw exception; + case QuicException e when (e.QuicError == QuicError.OperationAborted && _connection.AbortException != null): + // we closed the connection already, propagate the AbortException + HttpRequestError httpRequestError = _connection.AbortException is HttpProtocolException + ? HttpRequestError.HttpProtocolError + : HttpRequestError.Unknown; + throw new HttpRequestException(httpRequestError, SR.net_http_client_execution_error, _connection.AbortException); + case HttpIOException: _connection.Abort(ex); ExceptionDispatchInfo.Throw(ex); // Rethrow. diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs index 16abc260386b42..5baccdd3785543 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs @@ -35,7 +35,10 @@ private async Task AssertProtocolErrorAsync(long errorCode, Func task) { Exception outerEx = await Assert.ThrowsAnyAsync(task); _output.WriteLine(outerEx.ToString()); - Assert.IsType(outerEx); + + HttpRequestException httpReqException = Assert.IsType(outerEx); + Assert.Equal(HttpRequestError.HttpProtocolError, httpReqException.HttpRequestError); + HttpProtocolException protocolEx = Assert.IsType(outerEx.InnerException); Assert.Equal(errorCode, protocolEx.ErrorCode); } @@ -1759,7 +1762,14 @@ public async Task ServerClosesOutboundControlStream_ClientClosesConnection(Close return; } await connection.OutboundControlStream.DisposeAsync(); - await connection.EstablishControlStreamAsync(Array.Empty()); + try + { + await connection.EstablishControlStreamAsync(Array.Empty()); + } + catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted && ex.ApplicationErrorCode == Http3LoopbackConnection.H3_CLOSED_CRITICAL_STREAM) + { + // Data race, connection closed between WaitAsync and EstablishControlStreamAsync. Ignore this. + } await Task.Delay(100); } }