Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new QuicException proposal #71432

Merged
merged 16 commits into from
Jul 12, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ public async Task ShutdownAsync(bool failCurrentRequest = false)
long firstInvalidStreamId = failCurrentRequest ? _currentStreamId : _currentStreamId + 4;
await _outboundControlStream.SendGoAwayFrameAsync(firstInvalidStreamId);
}
catch (QuicConnectionAbortedException abortException) when (abortException.ErrorCode == H3_NO_ERROR)
catch (QuicException abortException) when (abortException.QuicError == QuicError.ConnectionAborted && abortException.ApplicationErrorCode == H3_NO_ERROR)
{
// Client must have closed the connection already because the HttpClientHandler instance was disposed.
// So nothing to do.
Expand Down Expand Up @@ -288,7 +288,7 @@ public async Task WaitForClientDisconnectAsync(bool refuseNewRequests = true)
throw new Exception("Unexpected request stream received while waiting for client disconnect");
}
}
catch (QuicConnectionAbortedException abortException) when (abortException.ErrorCode == H3_NO_ERROR)
catch (QuicException abortException) when (abortException.QuicError == QuicError.ConnectionAborted && abortException.ApplicationErrorCode == H3_NO_ERROR)
{
break;
}
Expand All @@ -301,7 +301,8 @@ public async Task WaitForClientDisconnectAsync(bool refuseNewRequests = true)

// The client's control stream should throw QuicConnectionAbortedException, indicating that it was
// aborted because the connection was closed (and was not explicitly closed or aborted prior to the connection being closed)
await Assert.ThrowsAsync<QuicConnectionAbortedException>(async () => await _inboundControlStream.ReadFrameAsync());
QuicException ex = await Assert.ThrowsAsync<QuicException>(async () => await _inboundControlStream.ReadFrameAsync());
Assert.Equal(QuicError.ConnectionAborted, ex.QuicError);

await CloseAsync(H3_NO_ERROR);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ async Task WaitForReadCancellation()
}
}
}
catch (QuicStreamAbortedException ex) when (ex.ErrorCode == Http3LoopbackConnection.H3_REQUEST_CANCELLED)
catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted && ex.ApplicationErrorCode == Http3LoopbackConnection.H3_REQUEST_CANCELLED)
{
readCanceled = true;
}
Expand All @@ -391,7 +391,7 @@ async Task WaitForWriteCancellation()
{
await _stream.WaitForWriteCompletionAsync();
}
catch (QuicStreamAbortedException ex) when (ex.ErrorCode == Http3LoopbackConnection.H3_REQUEST_CANCELLED)
catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted && ex.ApplicationErrorCode == Http3LoopbackConnection.H3_REQUEST_CANCELLED)
{
writeCanceled = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, lon
// Swallow any exceptions caused by the connection being closed locally or even disposed due to a race.
// Since quicStream will stay `null`, the code below will throw appropriate exception to retry the request.
catch (ObjectDisposedException) { }
catch (QuicException e) when (!(e is QuicConnectionAbortedException)) { }
catch (QuicException e) when (e.QuicError != QuicError.OperationAborted) { }
finally
{
if (HttpTelemetry.Log.IsEnabled() && queueStartingTimestamp != 0)
Expand Down Expand Up @@ -232,11 +232,13 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, lon

return await responseTask.ConfigureAwait(false);
}
catch (QuicConnectionAbortedException ex)
catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted)
{
Debug.Assert(ex.ApplicationErrorCode.HasValue);

// This will happen if we aborted _connection somewhere.
Abort(ex);
throw new HttpRequestException(SR.Format(SR.net_http_http3_connection_error, ex.ErrorCode), ex, RequestRetryType.RetryOnConnectionFailure);
throw new HttpRequestException(SR.Format(SR.net_http_http3_connection_error, ex.ApplicationErrorCode.Value), ex, RequestRetryType.RetryOnConnectionFailure);
}
finally
{
Expand Down Expand Up @@ -417,7 +419,7 @@ private async Task AcceptStreamsAsync()
_ = ProcessServerStreamAsync(stream);
}
}
catch (QuicOperationAbortedException)
catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted)
{
// Shutdown initiated by us, no need to abort.
}
Expand Down Expand Up @@ -452,7 +454,7 @@ private async Task ProcessServerStreamAsync(QuicStream stream)
{
bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(false);
}
catch (QuicStreamAbortedException)
catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted)
{
// Treat identical to receiving 0. See below comment.
bytesRead = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,23 +228,27 @@ await Task.WhenAny(sendContentTask, readResponseTask).ConfigureAwait(false) == s
shouldCancelBody = false;
return response;
}
catch (QuicStreamAbortedException ex) when (ex.ErrorCode == (long)Http3ErrorCode.VersionFallback)
catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted)
{
// The server is requesting us fall back to an older HTTP version.
throw new HttpRequestException(SR.net_http_retry_on_older_version, ex, RequestRetryType.RetryOnLowerHttpVersion);
}
catch (QuicStreamAbortedException ex) when (ex.ErrorCode == (long)Http3ErrorCode.RequestRejected)
{
// The server is rejecting the request without processing it, retry it on a different connection.
throw new HttpRequestException(SR.net_http_request_aborted, ex, RequestRetryType.RetryOnConnectionFailure);
}
catch (QuicStreamAbortedException ex)
{
// Our stream was reset.
Exception? abortException = _connection.AbortException;
throw new HttpRequestException(SR.net_http_client_execution_error, abortException ?? ex);
Debug.Assert(ex.ApplicationErrorCode.HasValue);

switch ((Http3ErrorCode)ex.ApplicationErrorCode.Value)
{
case Http3ErrorCode.VersionFallback:
// The server is requesting us fall back to an older HTTP version.
throw new HttpRequestException(SR.net_http_retry_on_older_version, ex, RequestRetryType.RetryOnLowerHttpVersion);

case Http3ErrorCode.RequestRejected:
// The server is rejecting the request without processing it, retry it on a different connection.
throw new HttpRequestException(SR.net_http_request_aborted, ex, RequestRetryType.RetryOnConnectionFailure);

default:
// Our stream was reset.
Exception? abortException = _connection.AbortException;
throw new HttpRequestException(SR.net_http_client_execution_error, abortException ?? ex);
}
}
catch (QuicConnectionAbortedException ex)
catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted)
{
// Our connection was reset. Start shutting down the connection.
Exception abortException = _connection.Abort(ex);
Expand Down Expand Up @@ -1185,12 +1189,10 @@ private void HandleReadResponseContentException(Exception ex, CancellationToken
{
switch (ex)
{
// Peer aborted the stream
case QuicStreamAbortedException:
// User aborted the stream
case QuicOperationAbortedException:
case QuicException e when (e.QuicError == QuicError.StreamAborted || e.QuicError == QuicError.OperationAborted):
// Peer or user aborted the stream
throw new IOException(SR.net_http_client_execution_error, new HttpRequestException(SR.net_http_client_execution_error, ex));
case QuicConnectionAbortedException:
case QuicException e when (e.QuicError == QuicError.ConnectionAborted):
// Our connection was reset. Start aborting the connection.
Exception abortException = _connection.Abort(ex);
throw new IOException(SR.net_http_client_execution_error, new HttpRequestException(SR.net_http_client_execution_error, abortException));
Expand All @@ -1202,10 +1204,10 @@ private void HandleReadResponseContentException(Exception ex, CancellationToken
_stream.AbortRead((long)Http3ErrorCode.RequestCancelled);
ExceptionDispatchInfo.Throw(ex); // Rethrow.
return; // Never reached.
default:
_stream.AbortRead((long)Http3ErrorCode.InternalError);
throw new IOException(SR.net_http_client_execution_error, new HttpRequestException(SR.net_http_client_execution_error, ex));
}

_stream.AbortRead((long)Http3ErrorCode.InternalError);
throw new IOException(SR.net_http_client_execution_error, new HttpRequestException(SR.net_http_client_execution_error, ex));
}

private async ValueTask<bool> ReadNextDataFrameAsync(HttpResponseMessage response, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,18 +277,18 @@ await server.AcceptConnectionAsync(async connection =>
[ActiveIssue("https://github.com/dotnet/runtime/issues/69870", TestPlatforms.Android)]
public async Task GetAsync_MissingExpires_ReturnNull()
{
await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
{
await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
{
using (HttpClient client = CreateHttpClient())
{
HttpResponseMessage response = await client.GetAsync(uri);
Assert.Null(response.Content.Headers.Expires);
}
},
async server =>
{
await server.HandleRequestAsync(HttpStatusCode.OK);
});
async server =>
{
await server.HandleRequestAsync(HttpStatusCode.OK);
});
}

[Theory]
Expand Down Expand Up @@ -434,7 +434,6 @@ await server.HandleRequestAsync(headers: new[]
});
}
catch (IOException) { }
catch (QuicConnectionAbortedException) { }
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Net.Sockets;
using System.Net.Test.Common;
using System.Reflection;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -277,13 +278,13 @@ public async Task ReservedFrameType_Throws()

await stream.SendFrameAsync(ReservedHttp2PriorityFrameId, new byte[8]);

QuicConnectionAbortedException ex = await Assert.ThrowsAsync<QuicConnectionAbortedException>(async () =>
QuicException ex = await AssertThrowsQuicExceptionAsync(QuicError.ConnectionAborted, async () =>
{
await stream.HandleRequestAsync();
using Http3LoopbackStream stream2 = await connection.AcceptRequestStreamAsync();
});

Assert.Equal(UnexpectedFrameErrorCode, ex.ErrorCode);
Assert.Equal(UnexpectedFrameErrorCode, ex.ApplicationErrorCode);
});

Task clientTask = Task.Run(async () =>
Expand Down Expand Up @@ -325,13 +326,13 @@ public async Task RequestSentResponseDisposed_ThrowsOnServer()
{
await stream.SendResponseBodyAsync(data, isFinal: false);
}
catch (QuicStreamAbortedException)
catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted)
{
hasFailed = true;
break;
}
}
Assert.True(hasFailed, $"Expected {nameof(QuicStreamAbortedException)}, instead ran successfully for {sw.Elapsed}");
Assert.True(hasFailed, $"Expected {nameof(QuicException)} with {nameof(QuicError.StreamAborted)}, instead ran successfully for {sw.Elapsed}");
});

Task clientTask = Task.Run(async () =>
Expand Down Expand Up @@ -384,13 +385,13 @@ public async Task RequestSendingResponseDisposed_ThrowsOnServer()
var (frameType, payload) = await stream.ReadFrameAsync();
Assert.Equal(Http3LoopbackStream.DataFrame, frameType);
}
catch (QuicStreamAbortedException)
catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted)
{
hasFailed = true;
break;
}
}
Assert.True(hasFailed, $"Expected {nameof(QuicStreamAbortedException)}, instead ran successfully for {sw.Elapsed}");
Assert.True(hasFailed, $"Expected {nameof(QuicException)} with {nameof(QuicError.StreamAborted)}, instead ran successfully for {sw.Elapsed}");
});

Task clientTask = Task.Run(async () =>
Expand Down Expand Up @@ -682,8 +683,8 @@ public async Task ResponseCancellation_ServerReceivesCancellation(CancellationTy
// In that case even with synchronization via semaphores, first writes after peer aborting may "succeed" (get SEND_COMPLETE event)
// We are asserting that PEER_RECEIVE_ABORTED would still arrive eventually

var ex = await Assert.ThrowsAsync<QuicStreamAbortedException>(() => SendDataForever(stream).WaitAsync(TimeSpan.FromSeconds(10)));
Assert.Equal(268, ex.ErrorCode);
var ex = await AssertThrowsQuicExceptionAsync(QuicError.StreamAborted, () => SendDataForever(stream).WaitAsync(TimeSpan.FromSeconds(10)));
Assert.Equal(268, ex.ApplicationErrorCode);

serverDone.Release();
});
Expand Down Expand Up @@ -724,7 +725,8 @@ public async Task ResponseCancellation_ServerReceivesCancellation(CancellationTy
{
var ioe = Assert.IsType<IOException>(ex);
var hre = Assert.IsType<HttpRequestException>(ioe.InnerException);
Assert.IsType<QuicOperationAbortedException>(hre.InnerException);
var qex = Assert.IsType<QuicException>(hre.InnerException);
Assert.Equal(QuicError.OperationAborted, qex.QuicError);
}

clientDone.Release();
Expand Down Expand Up @@ -762,9 +764,9 @@ public async Task ResponseCancellation_BothCancellationTokenAndDispose_Success()
// In that case even with synchronization via semaphores, first writes after peer aborting may "succeed" (get SEND_COMPLETE event)
// We are asserting that PEER_RECEIVE_ABORTED would still arrive eventually

var ex = await Assert.ThrowsAsync<QuicStreamAbortedException>(() => SendDataForever(stream).WaitAsync(TimeSpan.FromSeconds(20)));
QuicException ex = await AssertThrowsQuicExceptionAsync(QuicError.StreamAborted, () => SendDataForever(stream).WaitAsync(TimeSpan.FromSeconds(20)));
// exact error code depends on who won the race
Assert.True(ex.ErrorCode == 268 /* cancellation */ || ex.ErrorCode == 0xffffffff /* disposal */, $"Expected 268 or 0xffffffff, got {ex.ErrorCode}");
Assert.True(ex.ApplicationErrorCode == 268 /* cancellation */ || ex.ApplicationErrorCode == 0xffffffff /* disposal */, $"Expected 268 or 0xffffffff, got {ex.ApplicationErrorCode}");

serverDone.Release();
});
Expand Down Expand Up @@ -797,7 +799,8 @@ public async Task ResponseCancellation_BothCancellationTokenAndDispose_Success()
{
var ioe = Assert.IsType<IOException>(ex);
var hre = Assert.IsType<HttpRequestException>(ioe.InnerException);
Assert.IsType<QuicOperationAbortedException>(hre.InnerException);
var qex = Assert.IsType<QuicException>(hre.InnerException);
Assert.Equal(QuicError.OperationAborted, qex.QuicError);
}

clientDone.Release();
Expand Down Expand Up @@ -877,7 +880,9 @@ public async Task Alpn_NonH3_NegotiationFailure()
};

HttpRequestException ex = await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(request).WaitAsync(TimeSpan.FromSeconds(10)));
Assert.Contains("ALPN_NEG_FAILURE", ex.Message);
Assert.IsType<AuthenticationException>(ex.InnerException);
// TODO: check that the exception was caused by ALPN mismatch
// Assert.Contains("ALPN_NEG_FAILURE", authEx.InnerException?.Message);

clientDone.Release();
});
Expand Down Expand Up @@ -1096,8 +1101,8 @@ public async Task RequestContentStreaming_Timeout_BothClientAndServerReceiveCanc
await clientTask.WaitAsync(TimeSpan.FromSeconds(120));

// server receives cancellation
var ex = await Assert.ThrowsAsync<QuicStreamAbortedException>(() => serverTask.WaitAsync(TimeSpan.FromSeconds(120)));
Assert.Equal(268 /*H3_REQUEST_CANCELLED (0x10C)*/, ex.ErrorCode);
QuicException ex = await AssertThrowsQuicExceptionAsync(QuicError.StreamAborted, () => serverTask.WaitAsync(TimeSpan.FromSeconds(120)));
Assert.Equal(268 /*H3_REQUEST_CANCELLED (0x10C)*/, ex.ApplicationErrorCode);

Assert.NotNull(serverStream);
serverStream.Dispose();
Expand Down Expand Up @@ -1161,8 +1166,8 @@ public async Task RequestContentStreaming_Cancellation_BothClientAndServerReceiv
await clientTask.WaitAsync(TimeSpan.FromSeconds(120));

// server receives cancellation
var ex = await Assert.ThrowsAsync<QuicStreamAbortedException>(() => serverTask.WaitAsync(TimeSpan.FromSeconds(120)));
Assert.Equal(268 /*H3_REQUEST_CANCELLED (0x10C)*/, ex.ErrorCode);
QuicException ex = await AssertThrowsQuicExceptionAsync(QuicError.StreamAborted, () => serverTask.WaitAsync(TimeSpan.FromSeconds(120)));
Assert.Equal(268 /*H3_REQUEST_CANCELLED (0x10C)*/, ex.ApplicationErrorCode);

Assert.NotNull(serverStream);
serverStream.Dispose();
Expand Down Expand Up @@ -1337,6 +1342,13 @@ public async Task DuplexStreaming_AbortByServer_StreamingCancelled(bool graceful
connection.Dispose();
}

private static async Task<QuicException> AssertThrowsQuicExceptionAsync(QuicError expectedError, Func<Task> testCode)
{
QuicException ex = await Assert.ThrowsAsync<QuicException>(testCode);
Assert.Equal(expectedError, ex.QuicError);
return ex;
}

public static TheoryData<HttpStatusCode, bool> StatusCodesTestData()
{
var statuses = Enum.GetValues(typeof(HttpStatusCode)).Cast<HttpStatusCode>().Where(s => s >= HttpStatusCode.OK); // exclude informational
Expand Down
Loading