diff --git a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs index 778919eb4863d1..8745bcbaa7dc05 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs @@ -423,9 +423,9 @@ private async Task DrainResponseData() } } - public void Abort(long errorCode) + public void Abort(long errorCode, QuicAbortDirection direction = QuicAbortDirection.Both) { - _stream.Abort(QuicAbortDirection.Both, errorCode); + _stream.Abort(direction, errorCode); } public async Task<(long? frameType, byte[] payload)> ReadFrameAsync() 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 9382047b6fb450..1726cffcdedf6a 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 @@ -222,6 +222,10 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s { await writesClosed.WaitAsync(_requestBodyCancellationSource.Token).ConfigureAwait(false); } + catch (QuicException qex) when (qex.QuicError == QuicError.StreamAborted && qex.ApplicationErrorCode == (long)Http3ErrorCode.NoError) + { + // The server doesn't need the whole request to respond so it's aborting its reading side gracefully, see https://datatracker.ietf.org/doc/html/rfc9114#section-4.1-15. + } catch (OperationCanceledException) { // If the request got cancelled before WritesClosed completed, avoid leaking an unobserved task exception. @@ -475,6 +479,10 @@ private async Task SendContentAsync(HttpContent content, CancellationToken cance if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestContentStop(bytesWritten); } + catch (HttpRequestException hex) when (hex.InnerException is QuicException qex && qex.QuicError == QuicError.StreamAborted && qex.ApplicationErrorCode == (long)Http3ErrorCode.NoError) + { + // The server doesn't need the whole request to respond so it's aborting its reading side gracefully, see https://datatracker.ietf.org/doc/html/rfc9114#section-4.1-15. + } finally { _requestSendCompleted = true; 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 ea67775c63b66c..f965dd9520da93 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs @@ -392,7 +392,7 @@ public async Task SendAsync_RequestRejected_ClientRetries() await using (Http3LoopbackConnection connection1 = (Http3LoopbackConnection)await server.EstablishGenericConnectionAsync()) { await using Http3LoopbackStream stream = await connection1.AcceptRequestStreamAsync(); - stream.Abort(0x10B); // H3_REQUEST_REJECTED + stream.Abort(Http3LoopbackConnection.H3_REQUEST_REJECTED); await stream.DisposeAsync(); // shutdown the connection gracefully via GOAWAY frame for good measure await connection1.ShutdownAsync(true); @@ -423,6 +423,41 @@ public async Task SendAsync_RequestRejected_ClientRetries() await new[] { clientTask, serverTask }.WhenAllOrAnyFailed(20_000); } + [Fact] + public async Task SendAsync_RequestAbortedNoError_ClientSucceeds() + { + using Http3LoopbackServer server = CreateHttp3LoopbackServer(); + string httpContent = "hello world"; + + Task serverTask = Task.Run(async () => + { + await using (Http3LoopbackConnection connection = (Http3LoopbackConnection)await server.EstablishGenericConnectionAsync()) + { + await using Http3LoopbackStream stream = await connection.AcceptRequestStreamAsync(); + stream.Abort(Http3LoopbackConnection.H3_NO_ERROR, QuicAbortDirection.Read); + await stream.SendResponseAsync(content: httpContent); + await stream.DisposeAsync(); + } + }); + + Task clientTask = Task.Run(async () => + { + using HttpClient client = CreateHttpClient(); + using HttpRequestMessage request = new() + { + Method = HttpMethod.Post, + RequestUri = server.Address, + Version = HttpVersion30, + VersionPolicy = HttpVersionPolicy.RequestVersionExact, + Content = new ByteAtATimeContent(64*1024) + }; + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal(httpContent, content); + }); + + await new[] { clientTask, serverTask }.WhenAllOrAnyFailed(20_000); + } [Fact] public async Task ServerClosesConnection_ResponseContentStream_ThrowsHttpProtocolException()