Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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()
Expand Down