From 8cad6160ee411b37516a38742924277c793de288 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 17 Jul 2023 03:00:42 +0200 Subject: [PATCH 1/9] implement HttpRequestError --- .../System/Net/Http/ResponseStreamTest.cs | 4 +- .../System.Net.Http/ref/System.Net.Http.cs | 26 +++- .../src/System.Net.Http.csproj | 2 + .../src/System/Net/Http/HttpContent.cs | 9 +- .../src/System/Net/Http/HttpIOException.cs | 30 ++++ .../System/Net/Http/HttpProtocolException.cs | 4 +- .../src/System/Net/Http/HttpRequestError.cs | 71 ++++++++++ .../System/Net/Http/HttpRequestException.cs | 31 +++- .../AuthenticationHelper.NtAuth.cs | 2 +- .../ChunkedEncodingReadStream.cs | 10 +- .../Http/SocketsHttpHandler/ConnectHelper.cs | 33 +++-- .../ContentLengthReadStream.cs | 4 +- .../SocketsHttpHandler/Http2Connection.cs | 33 +++-- .../Http/SocketsHttpHandler/Http2Stream.cs | 16 +-- .../SocketsHttpHandler/Http3RequestStream.cs | 54 ++++--- .../Http/SocketsHttpHandler/HttpConnection.cs | 28 ++-- .../SocketsHttpHandler/HttpConnectionBase.cs | 2 +- .../SocketsHttpHandler/HttpConnectionPool.cs | 16 +-- .../Http/SocketsHttpHandler/SocksHelper.cs | 1 + .../HttpClientHandlerTest.Http2.cs | 19 +++ .../HttpClientHandlerTest.Http3.cs | 2 +- ...etsHttpHandlerTest.Http2ExtendedConnect.cs | 7 +- .../FunctionalTests/SocketsHttpHandlerTest.cs | 134 ++++++++++++++++++ .../System.Net.Http.Unit.Tests.csproj | 4 + .../Net/WebSockets/WebSocketHandle.Managed.cs | 2 +- .../tests/ConnectTest.Http2.cs | 12 +- 26 files changed, 438 insertions(+), 118 deletions(-) create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs diff --git a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs index 60153c0b4975d8..b5f760c8db00af 100644 --- a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs @@ -275,7 +275,9 @@ public async Task ReadAsStreamAsync_InvalidServerResponse_ThrowsIOException( { await StartTransferTypeAndErrorServer(transferType, transferError, async uri => { - await Assert.ThrowsAsync(() => ReadAsStreamHelper(uri)); + HttpIOException exception = await Assert.ThrowsAsync(() => ReadAsStreamHelper(uri)); + _output.WriteLine(exception.Message); + Assert.Equal(HttpRequestError.ResponseEnded, exception.HttpRequestError); }); } diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index ed341d31d8c0d3..840de295437f93 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -203,6 +203,11 @@ protected virtual void SerializeToStream(System.IO.Stream stream, System.Net.Tra protected virtual System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; } protected internal abstract bool TryComputeLength(out long length); } + public class HttpIOException : System.IO.IOException + { + public System.Net.Http.HttpRequestError HttpRequestError { get { throw null; } } + public HttpIOException(System.Net.Http.HttpRequestError httpRequestError, string? message = null, System.Exception? innerException = null) { } + } public abstract partial class HttpMessageHandler : System.IDisposable { protected HttpMessageHandler() { } @@ -241,17 +246,34 @@ public HttpMethod(string method) { } public static bool operator !=(System.Net.Http.HttpMethod? left, System.Net.Http.HttpMethod? right) { throw null; } public override string ToString() { throw null; } } - public sealed class HttpProtocolException : System.IO.IOException + public sealed class HttpProtocolException : HttpIOException { - public HttpProtocolException(long errorCode, string? message, System.Exception? innerException) { } + public HttpProtocolException(long errorCode, string? message, System.Exception? innerException) : base (default(System.Net.Http.HttpRequestError), default(string?), default(System.Exception?)) { } public long ErrorCode { get { throw null; } } } + public enum HttpRequestError + { + Unknown = 0, + NameResolutionError, + ConnectionError, + SecureConnectionError, + HttpProtocolError, + ExtendedConnectNotSupported, + VersionNegotiationError, + UserAuthenticationError, + ProxyTunnelError, + InvalidResponse, + ResponseEnded, + ConfigurationLimitExceeded, + } public partial class HttpRequestException : System.Exception { public HttpRequestException() { } public HttpRequestException(string? message) { } public HttpRequestException(string? message, System.Exception? inner) { } public HttpRequestException(string? message, System.Exception? inner, System.Net.HttpStatusCode? statusCode) { } + public HttpRequestException(string? message, Exception? inner = null, HttpStatusCode? statusCode = null, HttpRequestError? httpRequestError = null) { } + public System.Net.Http.HttpRequestError? HttpRequestError { get { throw null; } } public System.Net.HttpStatusCode? StatusCode { get { throw null; } } } public partial class HttpRequestMessage : System.IDisposable diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index de2be83a18d84f..f9c229575e1214 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -58,10 +58,12 @@ + + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs index 13e412205cec46..b1cc357f32a355 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs @@ -638,7 +638,7 @@ private bool CreateTemporaryBuffer(long maxBufferSize, out MemoryStream? tempBuf if (contentLength > maxBufferSize) { - error = new HttpRequestException(SR.Format(System.Globalization.CultureInfo.InvariantCulture, SR.net_http_content_buffersize_exceeded, maxBufferSize)); + error = CreateOverCapacityException(maxBufferSize); return null; } @@ -719,7 +719,8 @@ private static Exception GetStreamCopyException(Exception originalException) internal static Exception WrapStreamCopyException(Exception e) { Debug.Assert(StreamCopyExceptionNeedsWrapping(e)); - return new HttpRequestException(SR.net_http_content_stream_copy_error, e); + HttpRequestError error = e is HttpIOException ioEx ? ioEx.HttpRequestError : HttpRequestError.Unknown; + return new HttpRequestException(SR.net_http_content_stream_copy_error, e, httpRequestError: error); } private static int GetPreambleLength(ArraySegment buffer, Encoding encoding) @@ -832,9 +833,9 @@ private static async Task WaitAndReturnAsync(Task wait return returnFunc(state); } - private static HttpRequestException CreateOverCapacityException(int maxBufferSize) + private static HttpRequestException CreateOverCapacityException(long maxBufferSize) { - return new HttpRequestException(SR.Format(SR.net_http_content_buffersize_exceeded, maxBufferSize)); + return new HttpRequestException(SR.Format(System.Globalization.CultureInfo.InvariantCulture, SR.net_http_content_buffersize_exceeded, maxBufferSize), httpRequestError: HttpRequestError.ConfigurationLimitExceeded); } internal sealed class LimitMemoryStream : MemoryStream diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs new file mode 100644 index 00000000000000..676084051225a9 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace System.Net.Http +{ + /// + /// An exception thrown when an error occurs while reading the response content stream. + /// + public class HttpIOException : IOException + { + /// + /// Gets the that caused the exception. + /// + public HttpRequestError HttpRequestError { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The that caused the exception. + /// The message string describing the error. + /// The exception that is the cause of the current exception. + public HttpIOException(HttpRequestError httpRequestError, string? message = null, Exception? innerException = null) + : base(message, innerException) + { + HttpRequestError = httpRequestError; + } + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpProtocolException.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpProtocolException.cs index 15d9eae82b00c4..66370f49e1c72c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpProtocolException.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpProtocolException.cs @@ -14,7 +14,7 @@ namespace System.Net.Http /// When calling methods on the stream returned by or /// , can be thrown directly. /// - public sealed class HttpProtocolException : IOException + public sealed class HttpProtocolException : HttpIOException { /// /// Initializes a new instance of the class with the specified error code, @@ -24,7 +24,7 @@ public sealed class HttpProtocolException : IOException /// The error message that explains the reason for the exception. /// The exception that is the cause of the current exception. public HttpProtocolException(long errorCode, string message, Exception? innerException) - : base(message, innerException) + : base(Http.HttpRequestError.HttpProtocolError, message, innerException) { ErrorCode = errorCode; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs new file mode 100644 index 00000000000000..883cd673d2951b --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Net.Http +{ + /// + /// Defines error categories representing the reason for or . + /// + public enum HttpRequestError + { + /// + /// A generic or unknown error occured. + /// + Unknown = 0, + + /// + /// The DNS name resolution failed. + /// + NameResolutionError, // DNS request failed + + /// + /// A transport-level failure occured while connecting to the remote endpoint. + /// + ConnectionError, + + /// + /// An error occured during the TLS handshake. + /// + SecureConnectionError, + + /// + /// An HTTP/2 or HTTP/3 protocol error occured. + /// + HttpProtocolError, // HTTP 2.0/3.0 protocol error occurred + + /// + /// Extended CONNECT for WebSockets over HTTP/2 is not supported by the peer. + /// + ExtendedConnectNotSupported, + + /// + /// Cannot negotiate the HTTP Version requested. + /// + VersionNegotiationError, + + /// + /// The authentication failed with the provided credentials. + /// + UserAuthenticationError, + + /// + /// An error occured while establishing a connection to the proxy tunnel. + /// + ProxyTunnelError, + + /// + /// An invalid or malformed response has been received. + /// + InvalidResponse, // General error in response/malformed response + + /// + /// The response ended prematurely. + /// + ResponseEnded, + + /// + /// The response exceeded a pre-configured limit such as or . + /// + ConfigurationLimitExceeded, + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestException.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestException.cs index a2b68319a21d4b..5623387adfbee8 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestException.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestException.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; -using System.IO; - namespace System.Net.Http { public class HttpRequestException : Exception @@ -11,11 +8,10 @@ public class HttpRequestException : Exception internal RequestRetryType AllowRetry { get; } = RequestRetryType.NoRetry; public HttpRequestException() - : this(null, null) { } public HttpRequestException(string? message) - : this(message, null) + : base(message) { } public HttpRequestException(string? message, Exception? inner) @@ -39,6 +35,27 @@ public HttpRequestException(string? message, Exception? inner, HttpStatusCode? s StatusCode = statusCode; } + /// + /// Initializes a new instance of the class with a specific message an inner exception, and an HTTP status code and an . + /// + /// A message that describes the current exception. + /// The inner exception. + /// The HTTP status code. + /// The that caused the exception. + public HttpRequestException(string? message, Exception? inner = null, HttpStatusCode? statusCode = null, HttpRequestError? httpRequestError = null) + : this(message, inner, statusCode) + { + HttpRequestError = httpRequestError; + } + + /// + /// Gets the that caused the exception. + /// + /// + /// The or if the underlying did not provide it. + /// + public HttpRequestError? HttpRequestError { get; } + /// /// Gets the HTTP status code to be returned with the exception. /// @@ -49,8 +66,8 @@ public HttpRequestException(string? message, Exception? inner, HttpStatusCode? s // This constructor is used internally to indicate that a request was not successfully sent due to an IOException, // and the exception occurred early enough so that the request may be retried on another connection. - internal HttpRequestException(string? message, Exception? inner, RequestRetryType allowRetry) - : this(message, inner) + internal HttpRequestException(string? message, Exception? inner, RequestRetryType allowRetry, HttpRequestError? httpRequestError = null) + : this(message, inner, httpRequestError: httpRequestError) { AllowRetry = allowRetry; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs index 4de834c4992d06..7ac830be4b889a 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs @@ -209,7 +209,7 @@ private static async Task SendWithNtAuthAsync(HttpRequestMe { isNewConnection = false; connection.Dispose(); - throw new HttpRequestException(SR.Format(SR.net_http_authvalidationfailure, statusCode), null, HttpStatusCode.Unauthorized); + throw new HttpRequestException(SR.Format(SR.net_http_authvalidationfailure, statusCode), null, HttpStatusCode.Unauthorized, HttpRequestError.UserAuthenticationError); } break; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs index bf6d08a0923665..207c431678c864 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs @@ -73,7 +73,7 @@ public override int Read(Span buffer) int bytesRead = _connection.Read(buffer.Slice(0, (int)Math.Min((ulong)buffer.Length, _chunkBytesRemaining))); if (bytesRead == 0) { - throw new IOException(SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _chunkBytesRemaining)); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _chunkBytesRemaining)); } _chunkBytesRemaining -= (ulong)bytesRead; if (_chunkBytesRemaining == 0) @@ -189,7 +189,7 @@ private async ValueTask ReadAsyncCore(Memory buffer, CancellationToke int bytesRead = await _connection.ReadAsync(buffer.Slice(0, (int)Math.Min((ulong)buffer.Length, _chunkBytesRemaining))).ConfigureAwait(false); if (bytesRead == 0) { - throw new IOException(SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _chunkBytesRemaining)); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _chunkBytesRemaining)); } _chunkBytesRemaining -= (ulong)bytesRead; if (_chunkBytesRemaining == 0) @@ -332,7 +332,7 @@ private int ReadChunksFromConnectionBuffer(Span buffer, CancellationTokenR // Parse the hex value from it. if (!Utf8Parser.TryParse(currentLine, out ulong chunkSize, out int bytesConsumed, 'X')) { - throw new IOException(SR.Format(SR.net_http_invalid_response_chunk_header_invalid, BitConverter.ToString(currentLine.ToArray()))); + throw new HttpIOException(HttpRequestError.InvalidResponse, SR.Format(SR.net_http_invalid_response_chunk_header_invalid, BitConverter.ToString(currentLine.ToArray()))); } _chunkBytesRemaining = chunkSize; @@ -386,7 +386,7 @@ private int ReadChunksFromConnectionBuffer(Span buffer, CancellationTokenR if (currentLine.Length != 0) { - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_chunk_terminator_invalid, Encoding.ASCII.GetString(currentLine))); + throw new HttpIOException(HttpRequestError.InvalidResponse, SR.Format(SR.net_http_invalid_response_chunk_terminator_invalid, Encoding.ASCII.GetString(currentLine))); } _state = ParsingState.ExpectChunkHeader; @@ -449,7 +449,7 @@ private static void ValidateChunkExtension(ReadOnlySpan lineAfterChunkSize } else if (c != ' ' && c != '\t') // not called out in the RFC, but WinHTTP allows it { - throw new IOException(SR.Format(SR.net_http_invalid_response_chunk_extension_invalid, BitConverter.ToString(lineAfterChunkSize.ToArray()))); + throw new HttpIOException(HttpRequestError.InvalidResponse, SR.Format(SR.net_http_invalid_response_chunk_extension_invalid, BitConverter.ToString(lineAfterChunkSize.ToArray()))); } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs index dc077c9d28837d..17380764689be0 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs @@ -7,6 +7,7 @@ using System.Net.Security; using System.Net.Sockets; using System.Runtime.Versioning; +using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -88,13 +89,7 @@ public static async ValueTask EstablishSslConnectionAsync(SslClientAu throw CancellationHelper.CreateOperationCanceledException(e, cancellationToken); } - HttpRequestException ex = new HttpRequestException(SR.net_http_ssl_connection_failed, e); - if (request.IsExtendedConnectRequest) - { - // Extended connect request is negotiating strictly for ALPN = "h2" because HttpClient is unaware of a possible downgrade. - // At this point, SSL connection for HTTP / 2 failed, and the exception should indicate the reason for the external client / user. - ex.Data["HTTP2_ENABLED"] = false; - } + HttpRequestException ex = new HttpRequestException(SR.net_http_ssl_connection_failed, e, httpRequestError: HttpRequestError.SecureConnectionError); throw ex; } @@ -134,11 +129,27 @@ public static async ValueTask ConnectQuicAsync(HttpRequestMessag } } - internal static Exception CreateWrappedException(Exception error, string host, int port, CancellationToken cancellationToken) + internal static Exception CreateWrappedException(Exception exception, string host, int port, CancellationToken cancellationToken) { - return CancellationHelper.ShouldWrapInOperationCanceledException(error, cancellationToken) ? - CancellationHelper.CreateOperationCanceledException(error, cancellationToken) : - new HttpRequestException($"{error.Message} ({host}:{port})", error, RequestRetryType.RetryOnNextProxy); + return CancellationHelper.ShouldWrapInOperationCanceledException(exception, cancellationToken) ? + CancellationHelper.CreateOperationCanceledException(exception, cancellationToken) : + new HttpRequestException($"{exception.Message} ({host}:{port})", exception, RequestRetryType.RetryOnNextProxy, DeduceError(exception)); + + static HttpRequestError DeduceError(Exception exception) + { + // TODO: Deduce quic errors from QuicException.TransportErrorCode once https://github.com/dotnet/runtime/issues/87262 is implemented. + if (exception is AuthenticationException) + { + return HttpRequestError.SecureConnectionError; + } + + if (exception is SocketException socketException && socketException.SocketErrorCode == SocketError.HostNotFound) + { + return HttpRequestError.NameResolutionError; + } + + return HttpRequestError.ConnectionError; + } } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs index 83b522d0ce8f5c..7840efb4e7eba8 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs @@ -38,7 +38,7 @@ public override int Read(Span buffer) if (bytesRead <= 0 && buffer.Length != 0) { // Unexpected end of response stream. - throw new IOException(SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _contentBytesRemaining)); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _contentBytesRemaining)); } Debug.Assert((ulong)bytesRead <= _contentBytesRemaining); @@ -100,7 +100,7 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation CancellationHelper.ThrowIfCancellationRequested(cancellationToken); // Unexpected end of response stream. - throw new IOException(SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _contentBytesRemaining)); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _contentBytesRemaining)); } Debug.Assert((ulong)bytesRead <= _contentBytesRemaining); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs index 88678ae0b8a976..52e24cf784cc13 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs @@ -248,6 +248,7 @@ public async ValueTask SetupAsync(CancellationToken cancellationToken) throw; } + // TODO: Review this case! throw new IOException(SR.net_http_http2_connection_not_established, e); } @@ -488,10 +489,10 @@ private async ValueTask ReadFrameAsync(bool initialFrame = false) return frameHeader; void ThrowPrematureEOF(int requiredBytes) => - throw new IOException(SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, requiredBytes - _incomingBuffer.ActiveLength)); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, requiredBytes - _incomingBuffer.ActiveLength)); void ThrowMissingFrame() => - throw new IOException(SR.net_http_invalid_response_missing_frame); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.net_http_invalid_response_missing_frame); } private async Task ProcessIncomingFramesAsync() @@ -523,10 +524,16 @@ private async Task ProcessIncomingFramesAsync() Debug.Assert(InitialSettingsReceived.Task.IsCompleted); } + catch (HttpProtocolException e) + { + InitialSettingsReceived.TrySetException(e); + throw; + } catch (Exception e) { - InitialSettingsReceived.TrySetException(new IOException(SR.net_http_http2_connection_not_established, e)); - throw new IOException(SR.net_http_http2_connection_not_established, e); + var ex = new HttpIOException(HttpRequestError.InvalidResponse, SR.net_http_http2_connection_not_established, e); + InitialSettingsReceived.TrySetException(ex); + throw ex; } // Keep processing frames as they arrive. @@ -2096,17 +2103,13 @@ await Task.WhenAny(requestBodyTask, responseHeadersTask).ConfigureAwait(false) = return http2Stream.GetAndClearResponse(); } - catch (Exception e) + catch (HttpIOException e) { - if (e is IOException || - e is ObjectDisposedException || - e is HttpProtocolException || - e is InvalidOperationException) - { - throw new HttpRequestException(SR.net_http_client_execution_error, e); - } - - throw; + throw new HttpRequestException(e.Message, e, httpRequestError: e.HttpRequestError); + } + catch (Exception e) when (e is IOException || e is ObjectDisposedException || e is InvalidOperationException) + { + throw new HttpRequestException(SR.net_http_client_execution_error, e, httpRequestError: HttpRequestError.Unknown); } } @@ -2206,7 +2209,7 @@ private static void ThrowRetry(string message, Exception? innerException = null) throw new HttpRequestException(message, innerException, allowRetry: RequestRetryType.RetryOnConnectionFailure); private static Exception GetRequestAbortedException(Exception? innerException = null) => - innerException as HttpProtocolException ?? new IOException(SR.net_http_request_aborted, innerException); + innerException as HttpIOException ?? new IOException(SR.net_http_request_aborted, innerException); [DoesNotReturn] private static void ThrowRequestAborted(Exception? innerException = null) => diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs index 81aeaa63a9e29f..2b770746218669 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs @@ -540,7 +540,7 @@ void IHttpStreamHeadersHandler.OnStaticIndexedHeader(int index) if (index <= LastHPackRequestPseudoHeaderId) { if (NetEventSource.Log.IsEnabled()) Trace($"Invalid request pseudo-header ID {index}."); - throw new HttpRequestException(SR.net_http_invalid_response); + throw new HttpRequestException(SR.net_http_invalid_response, httpRequestError: HttpRequestError.InvalidResponse); } else if (index <= LastHPackStatusPseudoHeaderId) { @@ -563,7 +563,7 @@ void IHttpStreamHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan value) if (_responseProtocolState != ResponseProtocolState.ExpectingHeaders && _responseProtocolState != ResponseProtocolState.ExpectingTrailingHeaders) { if (NetEventSource.Log.IsEnabled()) Trace("Received header before status."); - throw new HttpRequestException(SR.net_http_invalid_response); + throw new HttpRequestException(SR.net_http_invalid_response, httpRequestError: HttpRequestError.InvalidResponse); } Encoding? valueEncoding = _connection._pool.Settings._responseHeaderEncodingSelector?.Invoke(descriptor.Name, _request); @@ -725,7 +725,7 @@ public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) else { if (NetEventSource.Log.IsEnabled()) Trace($"Invalid response pseudo-header '{Encoding.ASCII.GetString(name)}'."); - throw new HttpRequestException(SR.net_http_invalid_response); + throw new HttpRequestException(SR.net_http_invalid_response, httpRequestError: HttpRequestError.InvalidResponse); } } else @@ -734,7 +734,7 @@ public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) if (!HeaderDescriptor.TryGet(name, out HeaderDescriptor descriptor)) { // Invalid header name - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_header_name, Encoding.ASCII.GetString(name))); + throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_header_name, Encoding.ASCII.GetString(name)), httpRequestError: HttpRequestError.InvalidResponse); } OnHeader(descriptor, value); 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 5bb3a2941cd6ff..b426c810cadfaa 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 @@ -252,15 +252,27 @@ await Task.WhenAny(sendContentTask, readResponseTask).ConfigureAwait(false) == s { 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); + throw new HttpRequestException(SR.net_http_retry_on_older_version, ex, RequestRetryType.RetryOnLowerHttpVersion, httpRequestError: HttpRequestError.VersionNegotiationError); 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); + throw new HttpRequestException(SR.net_http_request_aborted, ex, RequestRetryType.RetryOnConnectionFailure, httpRequestError: HttpRequestError.Unknown); default: // Our stream was reset. - throw new HttpRequestException(SR.net_http_client_execution_error, _connection.AbortException ?? HttpProtocolException.CreateHttp3StreamException(code)); + HttpRequestError httpRequestError; + Exception innerException; + if (_connection.AbortException != null) + { + httpRequestError = HttpRequestError.Unknown; + innerException = _connection.AbortException; + } + else + { + httpRequestError = HttpRequestError.HttpProtocolError; + innerException = HttpProtocolException.CreateHttp3StreamException(code); + } + throw new HttpRequestException(SR.net_http_client_execution_error, innerException, httpRequestError: httpRequestError); } } catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted) @@ -270,12 +282,12 @@ await Task.WhenAny(sendContentTask, readResponseTask).ConfigureAwait(false) == s Http3ErrorCode code = (Http3ErrorCode)ex.ApplicationErrorCode.Value; Exception abortException = _connection.Abort(HttpProtocolException.CreateHttp3ConnectionException(code, SR.net_http_http3_connection_close)); - throw new HttpRequestException(SR.net_http_client_execution_error, abortException); + throw new HttpRequestException(SR.net_http_client_execution_error, abortException, httpRequestError: HttpRequestError.HttpProtocolError); } catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted && _connection.AbortException != null) { // we close the connection, propagate the AbortException - throw new HttpRequestException(SR.net_http_client_execution_error, _connection.AbortException); + throw new HttpRequestException(SR.net_http_client_execution_error, _connection.AbortException, httpRequestError: HttpRequestError.Unknown); } // It is possible for user's Content code to throw an unexpected OperationCanceledException. catch (OperationCanceledException ex) when (ex.CancellationToken == _requestBodyCancellationSource.Token || ex.CancellationToken == cancellationToken) @@ -289,14 +301,13 @@ await Task.WhenAny(sendContentTask, readResponseTask).ConfigureAwait(false) == s else { Debug.Assert(_requestBodyCancellationSource.IsCancellationRequested); - throw new HttpRequestException(SR.net_http_request_aborted, ex, RequestRetryType.RetryOnConnectionFailure); + throw new HttpRequestException(SR.net_http_request_aborted, ex, RequestRetryType.RetryOnConnectionFailure, httpRequestError: HttpRequestError.Unknown); } } - catch (HttpProtocolException ex) + catch (HttpIOException ex) { - // A connection-level protocol error has occurred on our stream. _connection.Abort(ex); - throw new HttpRequestException(SR.net_http_client_execution_error, ex); + throw new HttpRequestException(SR.net_http_client_execution_error, ex, httpRequestError: ex.HttpRequestError); } catch (Exception ex) { @@ -305,7 +316,7 @@ await Task.WhenAny(sendContentTask, readResponseTask).ConfigureAwait(false) == s { throw; } - throw new HttpRequestException(SR.net_http_client_execution_error, ex); + throw new HttpRequestException(SR.net_http_client_execution_error, ex, httpRequestError: HttpRequestError.Unknown); } finally { @@ -342,7 +353,7 @@ private async Task ReadResponseAsync(CancellationToken cancellationToken) { Trace($"Expected HEADERS as first response frame; received {frameType}."); } - throw new HttpRequestException(SR.net_http_invalid_response); + throw new HttpIOException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response); } await ReadHeadersAsync(payloadLength, cancellationToken).ConfigureAwait(false); @@ -528,7 +539,7 @@ private async ValueTask DrainContentLength0Frames(CancellationToken cancellation { Trace("Response content exceeded Content-Length."); } - throw new HttpRequestException(SR.net_http_invalid_response); + throw new HttpIOException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response); } break; default: @@ -824,7 +835,7 @@ private void BufferBytes(ReadOnlySpan span) else { // Our buffer has partial frame data in it but not enough to complete the read: bail out. - throw new HttpRequestException(SR.net_http_invalid_response_premature_eof); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.net_http_invalid_response_premature_eof); } } @@ -868,7 +879,7 @@ private async ValueTask ReadHeadersAsync(long headersLength, CancellationToken c if (headersLength > _headerBudgetRemaining) { _stream.Abort(QuicAbortDirection.Read, (long)Http3ErrorCode.ExcessiveLoad); - throw new HttpRequestException(SR.Format(SR.net_http_response_headers_exceeded_length, _connection.Pool.Settings.MaxResponseHeadersByteLength)); + throw new HttpRequestException(SR.Format(SR.net_http_response_headers_exceeded_length, _connection.Pool.Settings.MaxResponseHeadersByteLength), httpRequestError: HttpRequestError.ConfigurationLimitExceeded); } _headerBudgetRemaining -= (int)headersLength; @@ -887,7 +898,7 @@ private async ValueTask ReadHeadersAsync(long headersLength, CancellationToken c else { if (NetEventSource.Log.IsEnabled()) Trace($"Server closed response stream before entire header payload could be read. {headersLength:N0} bytes remaining."); - throw new HttpRequestException(SR.net_http_invalid_response_premature_eof); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.net_http_invalid_response_premature_eof); } } @@ -909,7 +920,7 @@ void IHttpStreamHeadersHandler.OnHeader(ReadOnlySpan name, ReadOnlySpan buffer) if (bytesRead == 0 && buffer.Length != 0) { - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _responseDataPayloadRemaining)); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _responseDataPayloadRemaining)); + //throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _responseDataPayloadRemaining)); } totalBytesRead += bytesRead; @@ -1219,7 +1231,8 @@ private async ValueTask ReadResponseContentAsync(HttpResponseMessage respon if (bytesRead == 0 && buffer.Length != 0) { - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _responseDataPayloadRemaining)); + //throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _responseDataPayloadRemaining)); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _responseDataPayloadRemaining)); } totalBytesRead += bytesRead; @@ -1263,8 +1276,7 @@ private void HandleReadResponseContentException(Exception ex, CancellationToken _connection.Abort(exception); throw exception; - case HttpProtocolException: - // A connection-level protocol error has occurred on our stream. + case HttpIOException: _connection.Abort(ex); ExceptionDispatchInfo.Throw(ex); // Rethrow. return; // Never reached. @@ -1276,7 +1288,7 @@ private void HandleReadResponseContentException(Exception ex, CancellationToken } _stream.Abort(QuicAbortDirection.Read, (long)Http3ErrorCode.InternalError); - throw new IOException(SR.net_http_client_execution_error, new HttpRequestException(SR.net_http_client_execution_error, ex)); + throw new HttpIOException(HttpRequestError.Unknown, SR.net_http_client_execution_error, new HttpRequestException(SR.net_http_client_execution_error, ex)); } private async ValueTask ReadNextDataFrameAsync(HttpResponseMessage response, CancellationToken cancellationToken) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs index 036012a0cf3553..1b5f1b10a5d360 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs @@ -621,7 +621,7 @@ public async Task SendAsync(HttpRequestMessage request, boo _canRetry = true; } - throw new IOException(SR.net_http_invalid_response_premature_eof); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.net_http_invalid_response_premature_eof); } @@ -1023,7 +1023,7 @@ private static void ParseStatusLineCore(Span line, HttpResponseMessage res const int MinStatusLineLength = 12; // "HTTP/1.x 123" if (line.Length < MinStatusLineLength || line[8] != ' ') { - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_line, Encoding.ASCII.GetString(line))); + throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_line, Encoding.ASCII.GetString(line)), httpRequestError: HttpRequestError.InvalidResponse); } ulong first8Bytes = BitConverter.ToUInt64(line); @@ -1044,7 +1044,7 @@ private static void ParseStatusLineCore(Span line, HttpResponseMessage res } else { - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_line, Encoding.ASCII.GetString(line))); + throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_line, Encoding.ASCII.GetString(line)), httpRequestError: HttpRequestError.InvalidResponse); } } @@ -1052,7 +1052,7 @@ private static void ParseStatusLineCore(Span line, HttpResponseMessage res byte status1 = line[9], status2 = line[10], status3 = line[11]; if (!IsDigit(status1) || !IsDigit(status2) || !IsDigit(status3)) { - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_code, Encoding.ASCII.GetString(line.Slice(9, 3)))); + throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_code, Encoding.ASCII.GetString(line.Slice(9, 3))), httpRequestError: HttpRequestError.InvalidResponse); } response.SetStatusCodeWithoutValidation((HttpStatusCode)(100 * (status1 - '0') + 10 * (status2 - '0') + (status3 - '0'))); @@ -1075,15 +1075,15 @@ private static void ParseStatusLineCore(Span line, HttpResponseMessage res { response.ReasonPhrase = HttpRuleParser.DefaultHttpEncoding.GetString(reasonBytes); } - catch (FormatException error) + catch (FormatException formatEx) { - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_reason, Encoding.ASCII.GetString(reasonBytes.ToArray())), error); + throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_reason, Encoding.ASCII.GetString(reasonBytes.ToArray())), formatEx, httpRequestError: HttpRequestError.InvalidResponse); } } } else { - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_line, Encoding.ASCII.GetString(line))); + throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_line, Encoding.ASCII.GetString(line)), httpRequestError: HttpRequestError.InvalidResponse); } } @@ -1182,7 +1182,7 @@ private bool ParseHeaders(HttpResponseMessage? response, bool isFromTrailer) } static void ThrowForInvalidHeaderLine(ReadOnlySpan buffer, int newLineIndex) => - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_header_line, Encoding.ASCII.GetString(buffer.Slice(0, newLineIndex)))); + throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_header_line, Encoding.ASCII.GetString(buffer.Slice(0, newLineIndex))), httpRequestError: HttpRequestError.InvalidResponse); } private void AddResponseHeader(ReadOnlySpan name, ReadOnlySpan value, HttpResponseMessage response, bool isFromTrailer) @@ -1281,14 +1281,14 @@ private void AddResponseHeader(ReadOnlySpan name, ReadOnlySpan value Debug.Assert(added); static void ThrowForEmptyHeaderName() => - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_header_name, "")); + throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_header_name, ""), httpRequestError: HttpRequestError.InvalidResponse); static void ThrowForInvalidHeaderName(ReadOnlySpan name) => - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_header_name, Encoding.ASCII.GetString(name))); + throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_header_name, Encoding.ASCII.GetString(name)), httpRequestError: HttpRequestError.InvalidResponse); } private void ThrowExceededAllowedReadLineBytes() => - throw new HttpRequestException(SR.Format(SR.net_http_response_headers_exceeded_length, _pool.Settings.MaxResponseHeadersByteLength)); + throw new HttpRequestException(SR.Format(SR.net_http_response_headers_exceeded_length, _pool.Settings.MaxResponseHeadersByteLength), httpRequestError: HttpRequestError.ConfigurationLimitExceeded); private void ProcessKeepAliveHeader(string keepAlive) { @@ -1611,7 +1611,7 @@ await _stream.ReadAsync(_readBuffer.AvailableMemory).ConfigureAwait(false) : if (NetEventSource.Log.IsEnabled()) Trace($"Received {bytesRead} bytes."); if (bytesRead == 0) { - throw new IOException(SR.net_http_invalid_response_premature_eof); + throw new HttpIOException(HttpRequestError.ResponseEnded, SR.net_http_invalid_response_premature_eof); } } @@ -2023,7 +2023,7 @@ public async ValueTask DrainResponseAsync(HttpResponseMessage response, Cancella if (_connectionClose) { - throw new HttpRequestException(SR.net_http_authconnectionfailure); + throw new HttpRequestException(SR.net_http_authconnectionfailure, httpRequestError: HttpRequestError.UserAuthenticationError); } Debug.Assert(response.Content != null); @@ -2039,7 +2039,7 @@ public async ValueTask DrainResponseAsync(HttpResponseMessage response, Cancella if (!await responseStream.DrainAsync(_pool.Settings._maxResponseDrainSize).ConfigureAwait(false) || _connectionClose) // Draining may have set this { - throw new HttpRequestException(SR.net_http_authconnectionfailure); + throw new HttpRequestException(SR.net_http_authconnectionfailure, httpRequestError: HttpRequestError.UserAuthenticationError); } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs index bf2b4a0a90a2cf..6b8e9712690060 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs @@ -158,7 +158,7 @@ internal static int ParseStatusCode(ReadOnlySpan value) !IsDigit(status2 = value[1]) || !IsDigit(status3 = value[2])) { - throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_code, System.Text.Encoding.ASCII.GetString(value))); + throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_code, System.Text.Encoding.ASCII.GetString(value)), httpRequestError: HttpRequestError.InvalidResponse); } return 100 * (status1 - '0') + 10 * (status2 - '0') + (status3 - '0'); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index f86147c5967401..86dbf0f6289193 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -447,13 +447,7 @@ private static void ThrowGetVersionException(HttpRequestMessage request, int des { Debug.Assert(desiredVersion == 2 || desiredVersion == 3); - HttpRequestException ex = new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, desiredVersion), inner); - if (request.IsExtendedConnectRequest && desiredVersion == 2) - { - ex.Data["HTTP2_ENABLED"] = false; - } - - throw ex; + throw new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, desiredVersion), inner, httpRequestError: HttpRequestError.VersionNegotiationError); } private bool CheckExpirationOnGet(HttpConnectionBase connection) @@ -1100,9 +1094,7 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn await connection.InitialSettingsReceived.WaitWithCancellationAsync(cancellationToken).ConfigureAwait(false); if (!connection.IsConnectEnabled) { - HttpRequestException exception = new(SR.net_unsupported_extended_connect); - exception.Data["SETTINGS_ENABLE_CONNECT_PROTOCOL"] = false; - throw exception; + throw new HttpRequestException(SR.net_unsupported_extended_connect, httpRequestError: HttpRequestError.ExtendedConnectNotSupported); } } @@ -1764,7 +1756,7 @@ private async ValueTask EstablishProxyTunnelAsync(bool async, Cancellati if (tunnelResponse.StatusCode != HttpStatusCode.OK) { tunnelResponse.Dispose(); - throw new HttpRequestException(SR.Format(SR.net_http_proxy_tunnel_returned_failure_status_code, _proxyUri, (int)tunnelResponse.StatusCode)); + throw new HttpRequestException(SR.Format(SR.net_http_proxy_tunnel_returned_failure_status_code, _proxyUri, (int)tunnelResponse.StatusCode), httpRequestError: HttpRequestError.ProxyTunnelError); } try @@ -1791,7 +1783,7 @@ private async ValueTask EstablishSocksTunnel(HttpRequestMessage request, catch (Exception e) when (!(e is OperationCanceledException)) { Debug.Assert(!(e is HttpRequestException)); - throw new HttpRequestException(SR.net_http_request_aborted, e); + throw new HttpRequestException(SR.net_http_request_aborted, e, httpRequestError: HttpRequestError.ProxyTunnelError); } return stream; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs index 25cee85afcd5a2..d261ee8d1e0118 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs @@ -359,6 +359,7 @@ private static async ValueTask ReadToFillAsync(Stream stream, Memory buffe if (bytesRead < buffer.Length) { + // TODO: How should we categorize this? Seems more like a connection establishment than than InvalidResponse throw new IOException(SR.net_http_invalid_response_premature_eof); } } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs index 224c116adbcb70..f58b6a06f38b07 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs @@ -32,13 +32,16 @@ private async Task AssertProtocolErrorAsync(Task task, ProtocolErrors errorCode) { HttpRequestException outerEx = await Assert.ThrowsAsync(() => task); _output.WriteLine(outerEx.InnerException.Message); + Assert.Equal(HttpRequestError.HttpProtocolError, outerEx.HttpRequestError); HttpProtocolException protocolEx = Assert.IsType(outerEx.InnerException); + Assert.Equal(HttpRequestError.HttpProtocolError, protocolEx.HttpRequestError); Assert.Equal(errorCode, (ProtocolErrors)protocolEx.ErrorCode); } private async Task AssertHttpProtocolException(Task task, ProtocolErrors errorCode) { HttpProtocolException protocolEx = await Assert.ThrowsAsync(() => task); + Assert.Equal(HttpRequestError.HttpProtocolError, protocolEx.HttpRequestError); Assert.Equal(errorCode, (ProtocolErrors)protocolEx.ErrorCode); } @@ -306,6 +309,22 @@ public async Task Http2_StreamResetByServerBeforeHeadersSent_RequestFails() } } + [ConditionalFact(nameof(SupportsAlpn))] + public async Task Http2_IncorrectServerPreface_RequestFailsWithAppropriateHttpProtocolException() + { + using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) + using (HttpClient client = CreateHttpClient()) + { + Task sendTask = client.GetAsync(server.Address); + + Http2LoopbackConnection connection = await server.AcceptConnectionAsync(); + await connection.ReadSettingsAsync(); + await connection.SendGoAway(0, ProtocolErrors.INTERNAL_ERROR); + + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.INTERNAL_ERROR); + } + } + [ConditionalFact(nameof(SupportsAlpn))] public async Task Http2_StreamResetByServerAfterHeadersSent_RequestFails() { 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 10427c96f7dc65..92c4ab0d6097f6 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs @@ -901,7 +901,7 @@ public async Task ResponseCancellation_ServerReceivesCancellation(CancellationTy } else { - var ioe = Assert.IsType(ex); + var ioe = Assert.IsType(ex); var hre = Assert.IsType(ioe.InnerException); var qex = Assert.IsType(hre.InnerException); Assert.Equal(QuicError.OperationAborted, qex.QuicError); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs index 207e7ecf6f2ba7..7215b02347720f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs @@ -90,8 +90,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => request.Headers.Protocol = "foo"; HttpRequestException ex = await Assert.ThrowsAsync(() => client.SendAsync(request)); - - Assert.Equal(false, ex.Data["SETTINGS_ENABLE_CONNECT_PROTOCOL"]); + Assert.Equal(HttpRequestError.ExtendedConnectNotSupported, ex.HttpRequestError); clientCompleted.SetResult(); }, @@ -154,12 +153,12 @@ await server.AcceptConnectionAsync(async connection => HttpRequestMessage request = CreateRequest(HttpMethod.Connect, server.Address, UseVersion, exactVersion: true); request.Headers.Protocol = "foo"; - Exception ex = await Assert.ThrowsAnyAsync(() => client.SendAsync(request)); + HttpRequestException ex = await Assert.ThrowsAsync(() => client.SendAsync(request)); clientCompleted.SetResult(); if (useSsl) { - Assert.Equal(false, ex.Data["HTTP2_ENABLED"]); + Assert.Equal(HttpRequestError.VersionNegotiationError, ex.HttpRequestError); } }); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index 9ec410e7c86054..068732d20e8a6e 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -4350,6 +4350,140 @@ public SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http3(ITestOutputHelpe protected override Version UseVersion => HttpVersion.Version30; } + public abstract class SocketsHttpHandler_HttpRequestErrorTest : HttpClientHandlerTestBase + { + protected SocketsHttpHandler_HttpRequestErrorTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task NameResolutionError() + { + using HttpClient client = CreateHttpClient(); + using HttpRequestMessage message = new(HttpMethod.Get, new Uri("https://BadHost")) + { + Version = UseVersion, + VersionPolicy = HttpVersionPolicy.RequestVersionExact + }; + + HttpRequestException ex = await Assert.ThrowsAsync(() => client.SendAsync(message)); + + if (UseVersion.Major < 3) + { + Assert.Equal(HttpRequestError.NameResolutionError, ex.HttpRequestError); + } + else + { + // System.Net.Quic does not report DNS resolution errors yet + Assert.Equal(HttpRequestError.ConnectionError, ex.HttpRequestError); + } + } + + [Fact] + public async Task ConnectionError() + { + if (UseVersion.Major == 3) + { + return; + } + using Socket notListening = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + notListening.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + int port = ((IPEndPoint)notListening.LocalEndPoint).Port; + Uri uri = new($"http://localhost:{port}"); + + using HttpClient client = CreateHttpClient(); + using HttpRequestMessage message = new(HttpMethod.Get, uri) + { + Version = UseVersion, + VersionPolicy = HttpVersionPolicy.RequestVersionExact + }; + + HttpRequestException ex = await Assert.ThrowsAsync(() => client.SendAsync(message)); + Assert.Equal(HttpRequestError.ConnectionError, ex.HttpRequestError); + } + + [Fact] + public async Task SecureConnectionError() + { + await LoopbackServerFactory.CreateClientAndServerAsync(async uri => + { + using HttpClientHandler handler = CreateHttpClientHandler(); + using HttpClient client = CreateHttpClient(handler); + GetUnderlyingSocketsHttpHandler(handler).SslOptions = new SslClientAuthenticationOptions() + { + RemoteCertificateValidationCallback = delegate { return false; }, + }; + using HttpRequestMessage message = new(HttpMethod.Get, uri) + { + Version = UseVersion, + VersionPolicy = HttpVersionPolicy.RequestVersionExact + }; + + HttpRequestException ex = await Assert.ThrowsAsync(() => client.SendAsync(message)); + Assert.Equal(HttpRequestError.SecureConnectionError, ex.HttpRequestError); + }, async server => + { + try + { + await server.AcceptConnectionAsync(_ => Task.CompletedTask); + } + catch + { + } + }, + options: new GenericLoopbackOptions() { UseSsl = true }); + } + + + } + + public sealed class SocketsHttpHandler_HttpRequestErrorTest_Http11 : SocketsHttpHandler_HttpRequestErrorTest + { + public SocketsHttpHandler_HttpRequestErrorTest_Http11(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version11; + } + + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))] + public sealed class SocketsHttpHandler_HttpRequestErrorTest_Http20 : SocketsHttpHandler_HttpRequestErrorTest + { + public SocketsHttpHandler_HttpRequestErrorTest_Http20(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version20; + + [Fact] + public async Task VersionNegitioationError() + { + await Http11LoopbackServerFactory.Singleton.CreateClientAndServerAsync(async uri => + { + using HttpClient client = CreateHttpClient(); + using HttpRequestMessage message = new(HttpMethod.Get, uri) + { + Version = UseVersion, + VersionPolicy = HttpVersionPolicy.RequestVersionExact + }; + + HttpRequestException ex = await Assert.ThrowsAsync(() => client.SendAsync(message)); + Assert.Equal(HttpRequestError.VersionNegotiationError, ex.HttpRequestError); + }, async server => + { + try + { + await server.AcceptConnectionAsync(_ => Task.CompletedTask); + } + catch + { + } + }, + options: new GenericLoopbackOptions() { UseSsl = true }); + } + } + + [ConditionalClass(typeof(HttpClientHandlerTestBase), nameof(IsQuicSupported))] + public sealed class SocketsHttpHandler_HttpRequestErrorTest_Http30 : SocketsHttpHandler_HttpRequestErrorTest + { + public SocketsHttpHandler_HttpRequestErrorTest_Http30(ITestOutputHelper output) : base(output) { } + protected override Version UseVersion => HttpVersion.Version30; + } + public class MySsl : SslStream { public MySsl(Stream stream) : base(stream) diff --git a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj index 2e702c527133a2..10221bf21670cc 100755 --- a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj @@ -192,6 +192,8 @@ Link="ProductionCode\System\Net\Http\HttpCompletionOption.cs" /> + + Task t = cws.ConnectAsync(uri, GetInvoker(), cts.Token); var ex = await Assert.ThrowsAnyAsync(() => t); - Assert.IsType(ex.InnerException); - Assert.True(ex.InnerException.Data.Contains("SETTINGS_ENABLE_CONNECT_PROTOCOL")); + HttpRequestException inner = Assert.IsType(ex.InnerException); + Assert.Equal(HttpRequestError.ExtendedConnectNotSupported, inner.HttpRequestError); } }, async server => @@ -100,8 +100,8 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => Task t = cws.ConnectAsync(uri, GetInvoker(), cts.Token); var ex = await Assert.ThrowsAnyAsync(() => t); - Assert.IsType(ex.InnerException); - Assert.True(ex.InnerException.Data.Contains("SETTINGS_ENABLE_CONNECT_PROTOCOL")); + HttpRequestException inner = Assert.IsType(ex.InnerException); + Assert.Equal(HttpRequestError.ExtendedConnectNotSupported, inner.HttpRequestError); } }, async server => @@ -124,8 +124,8 @@ public async Task ConnectAsync_Http11Server_DowngradeFail() Task t = cws.ConnectAsync(Test.Common.Configuration.WebSockets.SecureRemoteEchoServer, GetInvoker(), cts.Token); var ex = await Assert.ThrowsAnyAsync(() => t); - Assert.IsType(ex.InnerException); - Assert.True(ex.InnerException.Data.Contains("HTTP2_ENABLED")); + HttpRequestException inner = Assert.IsType(ex.InnerException); + Assert.Equal(HttpRequestError.SecureConnectionError, inner.HttpRequestError); Assert.Equal(WebSocketState.Closed, cws.State); } } From 511e16c3f9faa1bdad0e04f7c9bf1b543147105e Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 17 Jul 2023 03:07:52 +0200 Subject: [PATCH 2/9] DisableParallelization for http3 tests --- .../tests/FunctionalTests/SocketsHttpHandlerTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index 068732d20e8a6e..42f05289076301 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -4477,6 +4477,7 @@ await Http11LoopbackServerFactory.Singleton.CreateClientAndServerAsync(async uri } } + [Collection(nameof(DisableParallelization))] [ConditionalClass(typeof(HttpClientHandlerTestBase), nameof(IsQuicSupported))] public sealed class SocketsHttpHandler_HttpRequestErrorTest_Http30 : SocketsHttpHandler_HttpRequestErrorTest { From 12e67e79809b3602c7b403469d189d7c12dbb292 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 17 Jul 2023 18:33:55 +0200 Subject: [PATCH 3/9] review suggestions --- src/libraries/System.Net.Http/ref/System.Net.Http.cs | 4 ++-- .../System.Net.Http/src/Resources/Strings.resx | 3 +++ .../src/System/Net/Http/HttpIOException.cs | 12 ++++++------ .../src/System/Net/Http/HttpRequestError.cs | 6 +++--- .../Net/Http/SocketsHttpHandler/Http2Connection.cs | 5 ++--- .../Http/SocketsHttpHandler/Http3RequestStream.cs | 2 -- .../Http/SocketsHttpHandler/HttpConnectionPool.cs | 2 +- .../Net/Http/SocketsHttpHandler/SocksHelper.cs | 1 - 8 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index 840de295437f93..3ac82158387eb4 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -246,7 +246,7 @@ public HttpMethod(string method) { } public static bool operator !=(System.Net.Http.HttpMethod? left, System.Net.Http.HttpMethod? right) { throw null; } public override string ToString() { throw null; } } - public sealed class HttpProtocolException : HttpIOException + public sealed class HttpProtocolException : System.Net.Http.HttpIOException { public HttpProtocolException(long errorCode, string? message, System.Exception? innerException) : base (default(System.Net.Http.HttpRequestError), default(string?), default(System.Exception?)) { } public long ErrorCode { get { throw null; } } @@ -272,7 +272,7 @@ public HttpRequestException() { } public HttpRequestException(string? message) { } public HttpRequestException(string? message, System.Exception? inner) { } public HttpRequestException(string? message, System.Exception? inner, System.Net.HttpStatusCode? statusCode) { } - public HttpRequestException(string? message, Exception? inner = null, HttpStatusCode? statusCode = null, HttpRequestError? httpRequestError = null) { } + public HttpRequestException(string? message, System.Exception? inner = null, System.Net.HttpStatusCode? statusCode = null, System.Net.Http.HttpRequestError? httpRequestError = null) { } public System.Net.Http.HttpRequestError? HttpRequestError { get { throw null; } } public System.Net.HttpStatusCode? StatusCode { get { throw null; } } } diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index e7c2b65366259f..a6da16a9d71bfa 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -564,6 +564,9 @@ The proxy tunnel request to proxy '{0}' failed with status code '{1}'." + + An error occured while establishing a connection to the proxy tunnel. + System.Net.Http is not supported on this platform. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs index 676084051225a9..36207c4b475d0a 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs @@ -6,15 +6,10 @@ namespace System.Net.Http { /// - /// An exception thrown when an error occurs while reading the response content stream. + /// An exception thrown when an error occurs while reading the response. /// public class HttpIOException : IOException { - /// - /// Gets the that caused the exception. - /// - public HttpRequestError HttpRequestError { get; } - /// /// Initializes a new instance of the class. /// @@ -26,5 +21,10 @@ public HttpIOException(HttpRequestError httpRequestError, string? message = null { HttpRequestError = httpRequestError; } + + /// + /// Gets the that caused the exception. + /// + public HttpRequestError HttpRequestError { get; } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs index 883cd673d2951b..34e098ef9f20f2 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs @@ -16,7 +16,7 @@ public enum HttpRequestError /// /// The DNS name resolution failed. /// - NameResolutionError, // DNS request failed + NameResolutionError, /// /// A transport-level failure occured while connecting to the remote endpoint. @@ -31,7 +31,7 @@ public enum HttpRequestError /// /// An HTTP/2 or HTTP/3 protocol error occured. /// - HttpProtocolError, // HTTP 2.0/3.0 protocol error occurred + HttpProtocolError, /// /// Extended CONNECT for WebSockets over HTTP/2 is not supported by the peer. @@ -56,7 +56,7 @@ public enum HttpRequestError /// /// An invalid or malformed response has been received. /// - InvalidResponse, // General error in response/malformed response + InvalidResponse, /// /// The response ended prematurely. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs index 52e24cf784cc13..d4676d56962886 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs @@ -531,9 +531,8 @@ private async Task ProcessIncomingFramesAsync() } catch (Exception e) { - var ex = new HttpIOException(HttpRequestError.InvalidResponse, SR.net_http_http2_connection_not_established, e); - InitialSettingsReceived.TrySetException(ex); - throw ex; + InitialSettingsReceived.TrySetException(new HttpIOException(HttpRequestError.InvalidResponse, SR.net_http_http2_connection_not_established, e)); + throw new HttpIOException(HttpRequestError.InvalidResponse, SR.net_http_http2_connection_not_established, e); } // Keep processing frames as they arrive. 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 b426c810cadfaa..5c998579776431 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 @@ -1159,7 +1159,6 @@ private int ReadResponseContent(HttpResponseMessage response, Span buffer) if (bytesRead == 0 && buffer.Length != 0) { throw new HttpIOException(HttpRequestError.ResponseEnded, SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _responseDataPayloadRemaining)); - //throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _responseDataPayloadRemaining)); } totalBytesRead += bytesRead; @@ -1231,7 +1230,6 @@ private async ValueTask ReadResponseContentAsync(HttpResponseMessage respon if (bytesRead == 0 && buffer.Length != 0) { - //throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _responseDataPayloadRemaining)); throw new HttpIOException(HttpRequestError.ResponseEnded, SR.Format(SR.net_http_invalid_response_premature_eof_bytecount, _responseDataPayloadRemaining)); } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 86dbf0f6289193..b59230b55a75de 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -1783,7 +1783,7 @@ private async ValueTask EstablishSocksTunnel(HttpRequestMessage request, catch (Exception e) when (!(e is OperationCanceledException)) { Debug.Assert(!(e is HttpRequestException)); - throw new HttpRequestException(SR.net_http_request_aborted, e, httpRequestError: HttpRequestError.ProxyTunnelError); + throw new HttpRequestException(SR.net_http_proxy_tunnel_error, e, httpRequestError: HttpRequestError.ProxyTunnelError); } return stream; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs index d261ee8d1e0118..25cee85afcd5a2 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs @@ -359,7 +359,6 @@ private static async ValueTask ReadToFillAsync(Stream stream, Memory buffe if (bytesRead < buffer.Length) { - // TODO: How should we categorize this? Seems more like a connection establishment than than InvalidResponse throw new IOException(SR.net_http_invalid_response_premature_eof); } } From 633d632fb5178245964b41d485cf6f64683657c1 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 17 Jul 2023 18:41:42 +0200 Subject: [PATCH 4/9] revert deletion of HTTP2_ENABLED --- .../System/Net/Http/SocketsHttpHandler/ConnectHelper.cs | 6 ++++++ .../Net/Http/SocketsHttpHandler/HttpConnectionPool.cs | 8 +++++++- .../SocketsHttpHandlerTest.Http2ExtendedConnect.cs | 1 + .../src/System/Net/WebSockets/WebSocketHandle.Managed.cs | 2 +- .../tests/ConnectTest.Http2.cs | 1 + 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs index 17380764689be0..2dbe558d971661 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs @@ -90,6 +90,12 @@ public static async ValueTask EstablishSslConnectionAsync(SslClientAu } HttpRequestException ex = new HttpRequestException(SR.net_http_ssl_connection_failed, e, httpRequestError: HttpRequestError.SecureConnectionError); + if (request.IsExtendedConnectRequest) + { + // Extended connect request is negotiating strictly for ALPN = "h2" because HttpClient is unaware of a possible downgrade. + // At this point, SSL connection for HTTP / 2 failed, and the exception should indicate the reason for the external client / user. + ex.Data["HTTP2_ENABLED"] = false; + } throw ex; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index b59230b55a75de..50042a74f168bd 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -447,7 +447,13 @@ private static void ThrowGetVersionException(HttpRequestMessage request, int des { Debug.Assert(desiredVersion == 2 || desiredVersion == 3); - throw new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, desiredVersion), inner, httpRequestError: HttpRequestError.VersionNegotiationError); + HttpRequestException ex = new(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, desiredVersion), inner, httpRequestError: HttpRequestError.VersionNegotiationError); + if (request.IsExtendedConnectRequest && desiredVersion == 2) + { + ex.Data["HTTP2_ENABLED"] = false; + } + + throw ex; } private bool CheckExpirationOnGet(HttpConnectionBase connection) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs index 7215b02347720f..7d6043be2c6928 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs @@ -159,6 +159,7 @@ await server.AcceptConnectionAsync(async connection => if (useSsl) { Assert.Equal(HttpRequestError.VersionNegotiationError, ex.HttpRequestError); + Assert.Equal(false, ex.Data["HTTP2_ENABLED"]); } }); diff --git a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Managed.cs b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Managed.cs index d33d86c489e15c..3301bfead64c75 100644 --- a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Managed.cs +++ b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Managed.cs @@ -135,7 +135,7 @@ public async Task ConnectAsync(Uri uri, HttpMessageInvoker? invoker, Cancellatio break; } catch (HttpRequestException ex) when - ((ex.HttpRequestError is HttpRequestError.ExtendedConnectNotSupported or HttpRequestError.VersionNegotiationError or HttpRequestError.SecureConnectionError) + ((ex.HttpRequestError == HttpRequestError.ExtendedConnectNotSupported || ex.Data.Contains("HTTP2_ENABLED")) && tryDowngrade && (options.HttpVersion == HttpVersion.Version11 || options.HttpVersionPolicy == HttpVersionPolicy.RequestVersionOrLower)) { diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs index 6637940461bed2..19d70634750be7 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs @@ -124,6 +124,7 @@ public async Task ConnectAsync_Http11Server_DowngradeFail() Task t = cws.ConnectAsync(Test.Common.Configuration.WebSockets.SecureRemoteEchoServer, GetInvoker(), cts.Token); var ex = await Assert.ThrowsAnyAsync(() => t); + Assert.True(ex.InnerException.Data.Contains("HTTP2_ENABLED")); HttpRequestException inner = Assert.IsType(ex.InnerException); Assert.Equal(HttpRequestError.SecureConnectionError, inner.HttpRequestError); Assert.Equal(WebSocketState.Closed, cws.State); From 9afb7900d89ec6421225ea84c3b9f8185f1843b9 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 17 Jul 2023 19:43:01 +0200 Subject: [PATCH 5/9] cleanup some HTTP/3 cases --- .../System/Net/Http/HttpProtocolException.cs | 5 +++-- .../SocketsHttpHandler/Http3RequestStream.cs | 22 +++++-------------- .../SocketsHttpHandler/HttpConnectionPool.cs | 2 +- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpProtocolException.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpProtocolException.cs index 66370f49e1c72c..e61ecba6305798 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpProtocolException.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpProtocolException.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO; +using System.Net.Quic; namespace System.Net.Http { @@ -47,10 +48,10 @@ internal static HttpProtocolException CreateHttp2ConnectionException(Http2Protoc return new HttpProtocolException((long)protocolError, message, null); } - internal static HttpProtocolException CreateHttp3StreamException(Http3ErrorCode protocolError) + internal static HttpProtocolException CreateHttp3StreamException(Http3ErrorCode protocolError, QuicException innerException) { string message = SR.Format(SR.net_http_http3_stream_error, GetName(protocolError), ((int)protocolError).ToString("x")); - return new HttpProtocolException((long)protocolError, message, null); + return new HttpProtocolException((long)protocolError, message, innerException); } internal static HttpProtocolException CreateHttp3ConnectionException(Http3ErrorCode protocolError, string? message = null) 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 5c998579776431..c1a68ddbbc2d6e 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 @@ -252,27 +252,17 @@ await Task.WhenAny(sendContentTask, readResponseTask).ConfigureAwait(false) == s { 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, httpRequestError: HttpRequestError.VersionNegotiationError); + 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, httpRequestError: HttpRequestError.Unknown); + HttpProtocolException rejectedException = HttpProtocolException.CreateHttp3StreamException(code, ex); + throw new HttpRequestException(SR.net_http_request_aborted, rejectedException, RequestRetryType.RetryOnConnectionFailure, httpRequestError: HttpRequestError.HttpProtocolError); default: // Our stream was reset. - HttpRequestError httpRequestError; - Exception innerException; - if (_connection.AbortException != null) - { - httpRequestError = HttpRequestError.Unknown; - innerException = _connection.AbortException; - } - else - { - httpRequestError = HttpRequestError.HttpProtocolError; - innerException = HttpProtocolException.CreateHttp3StreamException(code); - } - throw new HttpRequestException(SR.net_http_client_execution_error, innerException, httpRequestError: httpRequestError); + var innerException = HttpProtocolException.CreateHttp3StreamException(code, ex); + throw new HttpRequestException(SR.net_http_client_execution_error, innerException, httpRequestError: HttpRequestError.HttpProtocolError); } } catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted) @@ -1265,7 +1255,7 @@ private void HandleReadResponseContentException(Exception ex, CancellationToken case QuicException e when (e.QuicError == QuicError.StreamAborted): // Peer aborted the stream Debug.Assert(e.ApplicationErrorCode.HasValue); - throw HttpProtocolException.CreateHttp3StreamException((Http3ErrorCode)e.ApplicationErrorCode.Value); + throw HttpProtocolException.CreateHttp3StreamException((Http3ErrorCode)e.ApplicationErrorCode.Value, e); case QuicException e when (e.QuicError == QuicError.ConnectionAborted): // Our connection was reset. Start aborting the connection. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 50042a74f168bd..0d585f3ac24887 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -1165,7 +1165,7 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn // Throw if fallback is not allowed by the version policy. if (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) { - throw new HttpRequestException(SR.Format(SR.net_http_requested_version_server_refused, request.Version, request.VersionPolicy), e); + throw new HttpRequestException(SR.Format(SR.net_http_requested_version_server_refused, request.Version, request.VersionPolicy), e, httpRequestError: HttpRequestError.VersionNegotiationError); } if (NetEventSource.Log.IsEnabled()) From c7ac73b5f9bd1be9873feb1093f1deff40a08dab Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 17 Jul 2023 19:58:23 +0200 Subject: [PATCH 6/9] test fixes --- .../tests/System/Net/Http/ResponseStreamTest.cs | 13 ++++++++++--- .../SocketsHttpHandlerTest.Http2ExtendedConnect.cs | 4 +--- .../tests/FunctionalTests/SocketsHttpHandlerTest.cs | 13 ++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs index b5f760c8db00af..78aede812369c5 100644 --- a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs @@ -275,9 +275,16 @@ public async Task ReadAsStreamAsync_InvalidServerResponse_ThrowsIOException( { await StartTransferTypeAndErrorServer(transferType, transferError, async uri => { - HttpIOException exception = await Assert.ThrowsAsync(() => ReadAsStreamHelper(uri)); - _output.WriteLine(exception.Message); - Assert.Equal(HttpRequestError.ResponseEnded, exception.HttpRequestError); + if (IsWinHttpHandler) + { + await Assert.ThrowsAsync(() => ReadAsStreamHelper(uri)); + } + else + { + HttpIOException exception = await Assert.ThrowsAsync(() => ReadAsStreamHelper(uri)); + Assert.Equal(HttpRequestError.ResponseEnded, exception.HttpRequestError); + } + }); } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs index 7d6043be2c6928..0ea6ae9e13f60b 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs @@ -153,12 +153,10 @@ await server.AcceptConnectionAsync(async connection => HttpRequestMessage request = CreateRequest(HttpMethod.Connect, server.Address, UseVersion, exactVersion: true); request.Headers.Protocol = "foo"; - HttpRequestException ex = await Assert.ThrowsAsync(() => client.SendAsync(request)); + Exception ex = await Assert.ThrowsAnyAsync(() => client.SendAsync(request)); clientCompleted.SetResult(); - if (useSsl) { - Assert.Equal(HttpRequestError.VersionNegotiationError, ex.HttpRequestError); Assert.Equal(false, ex.Data["HTTP2_ENABLED"]); } }); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index 42f05289076301..e8784be8ba487b 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -4350,6 +4350,7 @@ public SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http3(ITestOutputHelpe protected override Version UseVersion => HttpVersion.Version30; } + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] public abstract class SocketsHttpHandler_HttpRequestErrorTest : HttpClientHandlerTestBase { protected SocketsHttpHandler_HttpRequestErrorTest(ITestOutputHelper output) : base(output) @@ -4368,15 +4369,9 @@ public async Task NameResolutionError() HttpRequestException ex = await Assert.ThrowsAsync(() => client.SendAsync(message)); - if (UseVersion.Major < 3) - { - Assert.Equal(HttpRequestError.NameResolutionError, ex.HttpRequestError); - } - else - { - // System.Net.Quic does not report DNS resolution errors yet - Assert.Equal(HttpRequestError.ConnectionError, ex.HttpRequestError); - } + // TODO: Some platforms fail to detect NameResolutionError reliably, we should investigate this. + // Also, System.Net.Quic does not report DNS resolution errors yet. + Assert.True(ex.HttpRequestError is HttpRequestError.NameResolutionError or HttpRequestError.ConnectionError); } [Fact] From 2eb3a4a8e136aeca554372d10f20d514898a56a4 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 17 Jul 2023 20:04:31 +0200 Subject: [PATCH 7/9] override HttpIOException.Message to include HttpRequestError --- .../System.Net.Http/src/System/Net/Http/HttpIOException.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs index 36207c4b475d0a..cb3a8984b3dab1 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs @@ -26,5 +26,8 @@ public HttpIOException(HttpRequestError httpRequestError, string? message = null /// Gets the that caused the exception. /// public HttpRequestError HttpRequestError { get; } + + /// + public override string Message => $"{base.Message} ({HttpRequestError})"; } } From 24dd331791dc74edd5968f1815b1937fa6e83882 Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 17 Jul 2023 22:32:08 +0200 Subject: [PATCH 8/9] fix http2 websocket test --- .../System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs index 19d70634750be7..b5c85db841a790 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs @@ -126,7 +126,10 @@ public async Task ConnectAsync_Http11Server_DowngradeFail() var ex = await Assert.ThrowsAnyAsync(() => t); Assert.True(ex.InnerException.Data.Contains("HTTP2_ENABLED")); HttpRequestException inner = Assert.IsType(ex.InnerException); - Assert.Equal(HttpRequestError.SecureConnectionError, inner.HttpRequestError); + HttpRequestError expectedError = PlatformDetection.SupportsAlpn ? + HttpRequestError.SecureConnectionError : + HttpRequestError.VersionNegotiationError; + Assert.Equal(expectedError, inner.HttpRequestError); Assert.Equal(WebSocketState.Closed, cws.State); } } From b54e8c72cb63de3549ae4bff268d88daa628404c Mon Sep 17 00:00:00 2001 From: antonfirsov Date: Mon, 17 Jul 2023 23:37:23 +0200 Subject: [PATCH 9/9] review findings --- .../System.Net.Http/src/Resources/Strings.resx | 2 +- .../src/System/Net/Http/HttpRequestError.cs | 12 ++++++------ .../Net/Http/Metrics/HttpMetricsEnrichmentContext.cs | 2 +- .../Http/SocketsHttpHandler/Http3RequestStream.cs | 5 +++-- .../Http/SocketsHttpHandler/HttpConnectionPool.cs | 4 +++- .../tests/ConnectTest.Http2.cs | 2 ++ 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index a6da16a9d71bfa..ea9007d9ca6cca 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -565,7 +565,7 @@ The proxy tunnel request to proxy '{0}' failed with status code '{1}'." - An error occured while establishing a connection to the proxy tunnel. + An error occurred while establishing a connection to the proxy tunnel. System.Net.Http is not supported on this platform. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs index 34e098ef9f20f2..e448bf01e94868 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs @@ -9,7 +9,7 @@ namespace System.Net.Http public enum HttpRequestError { /// - /// A generic or unknown error occured. + /// A generic or unknown error occurred. /// Unknown = 0, @@ -19,17 +19,17 @@ public enum HttpRequestError NameResolutionError, /// - /// A transport-level failure occured while connecting to the remote endpoint. + /// A transport-level failure occurred while connecting to the remote endpoint. /// ConnectionError, /// - /// An error occured during the TLS handshake. + /// An error occurred during the TLS handshake. /// SecureConnectionError, /// - /// An HTTP/2 or HTTP/3 protocol error occured. + /// An HTTP/2 or HTTP/3 protocol error occurred. /// HttpProtocolError, @@ -44,12 +44,12 @@ public enum HttpRequestError VersionNegotiationError, /// - /// The authentication failed with the provided credentials. + /// The authentication failed. /// UserAuthenticationError, /// - /// An error occured while establishing a connection to the proxy tunnel. + /// An error occurred while establishing a connection to the proxy tunnel. /// ProxyTunnelError, diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/HttpMetricsEnrichmentContext.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/HttpMetricsEnrichmentContext.cs index 6ab0e14747468e..77b94fda296313 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/HttpMetricsEnrichmentContext.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/HttpMetricsEnrichmentContext.cs @@ -53,7 +53,7 @@ public sealed class HttpMetricsEnrichmentContext public HttpResponseMessage? Response => _response; /// - /// Gets the exception that occured or if there was no error. + /// Gets the exception that occurred or if there was no error. /// /// /// This property must not be used from outside of the enrichment callbacks. 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 c1a68ddbbc2d6e..8428acf02bfe2d 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 @@ -261,8 +261,9 @@ await Task.WhenAny(sendContentTask, readResponseTask).ConfigureAwait(false) == s default: // Our stream was reset. - var innerException = HttpProtocolException.CreateHttp3StreamException(code, ex); - throw new HttpRequestException(SR.net_http_client_execution_error, innerException, httpRequestError: HttpRequestError.HttpProtocolError); + Exception innerException = _connection.AbortException ?? HttpProtocolException.CreateHttp3StreamException(code, ex); + HttpRequestError httpRequestError = innerException is HttpProtocolException ? HttpRequestError.HttpProtocolError : HttpRequestError.Unknown; + throw new HttpRequestException(SR.net_http_client_execution_error, innerException, httpRequestError: httpRequestError); } } catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 0d585f3ac24887..e3650a6b4eecfe 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -1100,7 +1100,9 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn await connection.InitialSettingsReceived.WaitWithCancellationAsync(cancellationToken).ConfigureAwait(false); if (!connection.IsConnectEnabled) { - throw new HttpRequestException(SR.net_unsupported_extended_connect, httpRequestError: HttpRequestError.ExtendedConnectNotSupported); + HttpRequestException exception = new(SR.net_unsupported_extended_connect, httpRequestError: HttpRequestError.ExtendedConnectNotSupported); + exception.Data["SETTINGS_ENABLE_CONNECT_PROTOCOL"] = false; + throw exception; } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs index b5c85db841a790..9de9b934b59986 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs @@ -77,6 +77,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => var ex = await Assert.ThrowsAnyAsync(() => t); HttpRequestException inner = Assert.IsType(ex.InnerException); Assert.Equal(HttpRequestError.ExtendedConnectNotSupported, inner.HttpRequestError); + Assert.True(ex.InnerException.Data.Contains("SETTINGS_ENABLE_CONNECT_PROTOCOL")); } }, async server => @@ -102,6 +103,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => var ex = await Assert.ThrowsAnyAsync(() => t); HttpRequestException inner = Assert.IsType(ex.InnerException); Assert.Equal(HttpRequestError.ExtendedConnectNotSupported, inner.HttpRequestError); + Assert.True(ex.InnerException.Data.Contains("SETTINGS_ENABLE_CONNECT_PROTOCOL")); } }, async server =>