diff --git a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs index 60153c0b4975d8..78aede812369c5 100644 --- a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs @@ -275,7 +275,16 @@ public async Task ReadAsStreamAsync_InvalidServerResponse_ThrowsIOException( { await StartTransferTypeAndErrorServer(transferType, transferError, async uri => { - await Assert.ThrowsAsync(() => ReadAsStreamHelper(uri)); + 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/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index ed341d31d8c0d3..3ac82158387eb4 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 : System.Net.Http.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, 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; } } } public partial class HttpRequestMessage : System.IDisposable diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index e7c2b65366259f..ea9007d9ca6cca 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 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.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..cb3a8984b3dab1 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs @@ -0,0 +1,33 @@ +// 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. + /// + public class HttpIOException : IOException + { + /// + /// 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; + } + + /// + /// Gets the that caused the exception. + /// + public HttpRequestError HttpRequestError { get; } + + /// + public override string Message => $"{base.Message} ({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..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 { @@ -14,7 +15,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 +25,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; } @@ -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/HttpRequestError.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs new file mode 100644 index 00000000000000..e448bf01e94868 --- /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 occurred. + /// + Unknown = 0, + + /// + /// The DNS name resolution failed. + /// + NameResolutionError, + + /// + /// A transport-level failure occurred while connecting to the remote endpoint. + /// + ConnectionError, + + /// + /// An error occurred during the TLS handshake. + /// + SecureConnectionError, + + /// + /// An HTTP/2 or HTTP/3 protocol error occurred. + /// + HttpProtocolError, + + /// + /// Extended CONNECT for WebSockets over HTTP/2 is not supported by the peer. + /// + ExtendedConnectNotSupported, + + /// + /// Cannot negotiate the HTTP Version requested. + /// + VersionNegotiationError, + + /// + /// The authentication failed. + /// + UserAuthenticationError, + + /// + /// An error occurred while establishing a connection to the proxy tunnel. + /// + ProxyTunnelError, + + /// + /// An invalid or malformed response has been received. + /// + InvalidResponse, + + /// + /// 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/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/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..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 @@ -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,7 +89,7 @@ public static async ValueTask EstablishSslConnectionAsync(SslClientAu throw CancellationHelper.CreateOperationCanceledException(e, cancellationToken); } - HttpRequestException ex = new HttpRequestException(SR.net_http_ssl_connection_failed, e); + 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. @@ -134,11 +135,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..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 @@ -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,15 @@ 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); + 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. @@ -2096,17 +2102,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 +2208,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..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 @@ -256,11 +256,14 @@ await Task.WhenAny(sendContentTask, readResponseTask).ConfigureAwait(false) == s 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); + 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. - throw new HttpRequestException(SR.net_http_client_execution_error, _connection.AbortException ?? HttpProtocolException.CreateHttp3StreamException(code)); + 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) @@ -270,12 +273,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 +292,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 +307,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 +344,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 +530,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 +826,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 +870,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 +889,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 +911,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)); } totalBytesRead += bytesRead; @@ -1219,7 +1221,7 @@ 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)); } totalBytesRead += bytesRead; @@ -1254,7 +1256,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. @@ -1263,8 +1265,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 +1277,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..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 @@ -447,7 +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); + 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; @@ -1100,7 +1100,7 @@ public async ValueTask SendWithVersionDetectionAndRetryAsyn await connection.InitialSettingsReceived.WaitWithCancellationAsync(cancellationToken).ConfigureAwait(false); if (!connection.IsConnectEnabled) { - HttpRequestException exception = new(SR.net_unsupported_extended_connect); + HttpRequestException exception = new(SR.net_unsupported_extended_connect, httpRequestError: HttpRequestError.ExtendedConnectNotSupported); exception.Data["SETTINGS_ENABLE_CONNECT_PROTOCOL"] = false; throw exception; } @@ -1167,7 +1167,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()) @@ -1764,7 +1764,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 +1791,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_proxy_tunnel_error, e, httpRequestError: HttpRequestError.ProxyTunnelError); } return stream; 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..0ea6ae9e13f60b 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(); }, @@ -156,7 +155,6 @@ await server.AcceptConnectionAsync(async connection => Exception ex = await Assert.ThrowsAnyAsync(() => client.SendAsync(request)); clientCompleted.SetResult(); - if (useSsl) { 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 9ec410e7c86054..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,136 @@ 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) + { + } + + [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)); + + // 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] + 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 }); + } + } + + [Collection(nameof(DisableParallelization))] + [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); + HttpRequestException inner = Assert.IsType(ex.InnerException); + Assert.Equal(HttpRequestError.ExtendedConnectNotSupported, inner.HttpRequestError); Assert.True(ex.InnerException.Data.Contains("SETTINGS_ENABLE_CONNECT_PROTOCOL")); } }, @@ -100,7 +101,8 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => Task t = cws.ConnectAsync(uri, GetInvoker(), cts.Token); var ex = await Assert.ThrowsAnyAsync(() => t); - Assert.IsType(ex.InnerException); + HttpRequestException inner = Assert.IsType(ex.InnerException); + Assert.Equal(HttpRequestError.ExtendedConnectNotSupported, inner.HttpRequestError); Assert.True(ex.InnerException.Data.Contains("SETTINGS_ENABLE_CONNECT_PROTOCOL")); } }, @@ -124,8 +126,12 @@ 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); + HttpRequestError expectedError = PlatformDetection.SupportsAlpn ? + HttpRequestError.SecureConnectionError : + HttpRequestError.VersionNegotiationError; + Assert.Equal(expectedError, inner.HttpRequestError); Assert.Equal(WebSocketState.Closed, cws.State); } }