Implement HttpRequestError (#88974)
authorAnton Firszov <antonfir@gmail.com>
Tue, 18 Jul 2023 13:30:47 +0000 (15:30 +0200)
committerGitHub <noreply@github.com>
Tue, 18 Jul 2023 13:30:47 +0000 (15:30 +0200)
Fixes #76644, fixes #82168.

27 files changed:
src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs
src/libraries/System.Net.Http/ref/System.Net.Http.cs
src/libraries/System.Net.Http/src/Resources/Strings.resx
src/libraries/System.Net.Http/src/System.Net.Http.csproj
src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs
src/libraries/System.Net.Http/src/System/Net/Http/HttpIOException.cs [new file with mode: 0644]
src/libraries/System.Net.Http/src/System/Net/Http/HttpProtocolException.cs
src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestError.cs [new file with mode: 0644]
src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestException.cs
src/libraries/System.Net.Http/src/System/Net/Http/Metrics/HttpMetricsEnrichmentContext.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ContentLengthReadStream.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs
src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs
src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.Http2ExtendedConnect.cs
src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs
src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj
src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Managed.cs
src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs

index 60153c0..78aede8 100644 (file)
@@ -275,7 +275,16 @@ namespace System.Net.Http.Functional.Tests
         {
             await StartTransferTypeAndErrorServer(transferType, transferError, async uri =>
             {
-                await Assert.ThrowsAsync<IOException>(() => ReadAsStreamHelper(uri));
+                if (IsWinHttpHandler)
+                {
+                    await Assert.ThrowsAsync<IOException>(() => ReadAsStreamHelper(uri));
+                }
+                else
+                {
+                    HttpIOException exception = await Assert.ThrowsAsync<HttpIOException>(() => ReadAsStreamHelper(uri));
+                    Assert.Equal(HttpRequestError.ResponseEnded, exception.HttpRequestError);
+                }
+                
             });
         }
 
index ed341d3..3ac8215 100644 (file)
@@ -203,6 +203,11 @@ namespace System.Net.Http
         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 @@ namespace System.Net.Http
         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
index e7c2b65..ea9007d 100644 (file)
   <data name="net_http_proxy_tunnel_returned_failure_status_code" xml:space="preserve">
     <value>The proxy tunnel request to proxy '{0}' failed with status code '{1}'."</value>
   </data>
+  <data name="net_http_proxy_tunnel_error" xml:space="preserve">
+    <value>An error occurred while establishing a connection to the proxy tunnel.</value>
+  </data>
   <data name="PlatformNotSupported_NetHttp" xml:space="preserve">
     <value>System.Net.Http is not supported on this platform.</value>
   </data>
index de2be83..f9c2295 100644 (file)
     <Compile Include="System\Net\Http\HttpParseResult.cs" />
     <Compile Include="System\Net\Http\HttpProtocolException.cs" />
     <Compile Include="System\Net\Http\HttpRequestException.cs" />
+    <Compile Include="System\Net\Http\HttpRequestError.cs" />
     <Compile Include="System\Net\Http\HttpRequestMessage.cs" />
     <Compile Include="System\Net\Http\HttpRequestOptions.cs" />
     <Compile Include="System\Net\Http\HttpRequestOptionsKey.cs" />
     <Compile Include="System\Net\Http\HttpResponseMessage.cs" />
+    <Compile Include="System\Net\Http\HttpIOException.cs" />
     <Compile Include="System\Net\Http\HttpRuleParser.cs" />
     <Compile Include="System\Net\Http\HttpTelemetry.cs" />
     <Compile Include="System\Net\Http\HttpVersionPolicy.cs" />
index 13e4122..b1cc357 100644 (file)
@@ -638,7 +638,7 @@ namespace System.Net.Http
 
                 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 @@ namespace System.Net.Http
         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<byte> buffer, Encoding encoding)
@@ -832,9 +833,9 @@ namespace System.Net.Http
             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 (file)
index 0000000..cb3a898
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// An exception thrown when an error occurs while reading the response.
+    /// </summary>
+    public class HttpIOException : IOException
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="HttpIOException"/> class.
+        /// </summary>
+        /// <param name="httpRequestError">The <see cref="Http.HttpRequestError"/> that caused the exception.</param>
+        /// <param name="message">The message string describing the error.</param>
+        /// <param name="innerException">The exception that is the cause of the current exception.</param>
+        public HttpIOException(HttpRequestError httpRequestError, string? message = null, Exception? innerException = null)
+            : base(message, innerException)
+        {
+            HttpRequestError = httpRequestError;
+        }
+
+        /// <summary>
+        /// Gets the <see cref="Http.HttpRequestError"/> that caused the exception.
+        /// </summary>
+        public HttpRequestError HttpRequestError { get; }
+
+        /// <inheritdoc />
+        public override string Message => $"{base.Message} ({HttpRequestError})";
+    }
+}
index 15d9eae..e61ecba 100644 (file)
@@ -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 <see cref="Stream"/> methods on the stream returned by <see cref="HttpContent.ReadAsStream()"/> or
     /// <see cref="HttpContent.ReadAsStreamAsync(Threading.CancellationToken)"/>, <see cref="HttpProtocolException"/> can be thrown directly.
     /// </remarks>
-    public sealed class HttpProtocolException : IOException
+    public sealed class HttpProtocolException : HttpIOException
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="HttpProtocolException"/> class with the specified error code,
@@ -24,7 +25,7 @@ namespace System.Net.Http
         /// <param name="message">The error message that explains the reason for the exception.</param>
         /// <param name="innerException">The exception that is the cause of the current exception.</param>
         public HttpProtocolException(long errorCode, string message, Exception? innerException)
-            : base(message, innerException)
+            : base(Http.HttpRequestError.HttpProtocolError, message, innerException)
         {
             ErrorCode = errorCode;
         }
@@ -47,10 +48,10 @@ namespace System.Net.Http
             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 (file)
index 0000000..e448bf0
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// Defines error categories representing the reason for <see cref="HttpRequestException"/> or <see cref="HttpIOException"/>.
+    /// </summary>
+    public enum HttpRequestError
+    {
+        /// <summary>
+        /// A generic or unknown error occurred.
+        /// </summary>
+        Unknown = 0,
+
+        /// <summary>
+        /// The DNS name resolution failed.
+        /// </summary>
+        NameResolutionError,
+
+        /// <summary>
+        /// A transport-level failure occurred while connecting to the remote endpoint.
+        /// </summary>
+        ConnectionError,
+
+        /// <summary>
+        /// An error occurred during the TLS handshake.
+        /// </summary>
+        SecureConnectionError,
+
+        /// <summary>
+        /// An HTTP/2 or HTTP/3 protocol error occurred.
+        /// </summary>
+        HttpProtocolError,
+
+        /// <summary>
+        /// Extended CONNECT for WebSockets over HTTP/2 is not supported by the peer.
+        /// </summary>
+        ExtendedConnectNotSupported,
+
+        /// <summary>
+        /// Cannot negotiate the HTTP Version requested.
+        /// </summary>
+        VersionNegotiationError,
+
+        /// <summary>
+        /// The authentication failed.
+        /// </summary>
+        UserAuthenticationError,
+
+        /// <summary>
+        /// An error occurred while establishing a connection to the proxy tunnel.
+        /// </summary>
+        ProxyTunnelError,
+
+        /// <summary>
+        /// An invalid or malformed response has been received.
+        /// </summary>
+        InvalidResponse,
+
+        /// <summary>
+        /// The response ended prematurely.
+        /// </summary>
+        ResponseEnded,
+
+        /// <summary>
+        /// The response exceeded a pre-configured limit such as <see cref="HttpClient.MaxResponseContentBufferSize"/> or <see cref="HttpClientHandler.MaxResponseHeadersLength"/>.
+        /// </summary>
+        ConfigurationLimitExceeded,
+    }
+}
index a2b6831..5623387 100644 (file)
@@ -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 @@ namespace System.Net.Http
         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)
@@ -40,6 +36,27 @@ namespace System.Net.Http
         }
 
         /// <summary>
+        /// Initializes a new instance of the <see cref="HttpRequestException" /> class with a specific message an inner exception, and an HTTP status code and an <see cref="HttpRequestError"/>.
+        /// </summary>
+        /// <param name="message">A message that describes the current exception.</param>
+        /// <param name="inner">The inner exception.</param>
+        /// <param name="statusCode">The HTTP status code.</param>
+        /// <param name="httpRequestError">The <see cref="HttpRequestError"/> that caused the exception.</param>
+        public HttpRequestException(string? message, Exception? inner = null, HttpStatusCode? statusCode = null, HttpRequestError? httpRequestError = null)
+            : this(message, inner, statusCode)
+        {
+            HttpRequestError = httpRequestError;
+        }
+
+        /// <summary>
+        /// Gets the <see cref="Http.HttpRequestError"/> that caused the exception.
+        /// </summary>
+        /// <value>
+        /// The <see cref="Http.HttpRequestError"/> or <see langword="null"/> if the underlying <see cref="HttpMessageHandler"/> did not provide it.
+        /// </value>
+        public HttpRequestError? HttpRequestError { get; }
+
+        /// <summary>
         /// Gets the HTTP status code to be returned with the exception.
         /// </summary>
         /// <value>
@@ -49,8 +66,8 @@ namespace System.Net.Http
 
         // 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;
         }
index 6ab0e14..77b94fd 100644 (file)
@@ -53,7 +53,7 @@ namespace System.Net.Http.Metrics
         public HttpResponseMessage? Response => _response;
 
         /// <summary>
-        /// Gets the exception that occured or <see langword="null"/> if there was no error.
+        /// Gets the exception that occurred or <see langword="null"/> if there was no error.
         /// </summary>
         /// <remarks>
         /// This property must not be used from outside of the enrichment callbacks.
index 4de834c..7ac830b 100644 (file)
@@ -209,7 +209,7 @@ namespace System.Net.Http
                                 {
                                     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;
                             }
index bf6d08a..207c431 100644 (file)
@@ -73,7 +73,7 @@ namespace System.Net.Http
                         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 @@ namespace System.Net.Http
                             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 @@ namespace System.Net.Http
                             // 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 @@ namespace System.Net.Http
 
                             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 @@ namespace System.Net.Http
                     }
                     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())));
                     }
                 }
             }
index dc077c9..2dbe558 100644 (file)
@@ -7,6 +7,7 @@ using System.Net.Quic;
 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 @@ namespace System.Net.Http
                     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 @@ namespace System.Net.Http
             }
         }
 
-        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;
+            }
         }
     }
 }
index 83b522d..7840efb 100644 (file)
@@ -38,7 +38,7 @@ namespace System.Net.Http
                 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 @@ namespace System.Net.Http
                     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);
index 064825a..6956310 100644 (file)
@@ -248,6 +248,7 @@ namespace System.Net.Http
                     throw;
                 }
 
+                // TODO: Review this case!
                 throw new IOException(SR.net_http_http2_connection_not_established, e);
             }
 
@@ -488,10 +489,10 @@ namespace System.Net.Http
             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 @@ namespace System.Net.Http
 
                     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 @@ namespace System.Net.Http
 
                 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 @@ namespace System.Net.Http
             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) =>
index 81aeaa6..2b77074 100644 (file)
@@ -540,7 +540,7 @@ namespace System.Net.Http
                 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 @@ namespace System.Net.Http
                 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)
                 {
@@ -589,7 +589,7 @@ namespace System.Net.Http
                 _headerBudgetRemaining -= amount;
                 if (_headerBudgetRemaining < 0)
                 {
-                    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);
                 }
             }
 
@@ -611,14 +611,14 @@ namespace System.Net.Http
                     if (_responseProtocolState == ResponseProtocolState.ExpectingHeaders)
                     {
                         if (NetEventSource.Log.IsEnabled()) Trace("Received extra status header.");
-                        throw new HttpRequestException(SR.net_http_invalid_response_multiple_status_codes);
+                        throw new HttpRequestException(SR.net_http_invalid_response_multiple_status_codes, httpRequestError: HttpRequestError.ConfigurationLimitExceeded);
                     }
 
                     if (_responseProtocolState != ResponseProtocolState.ExpectingStatus)
                     {
                         // Pseudo-headers are allowed only in header block
                         if (NetEventSource.Log.IsEnabled()) Trace($"Status pseudo-header received in {_responseProtocolState} state.");
-                        throw new HttpRequestException(SR.net_http_invalid_response_pseudo_header_in_trailer);
+                        throw new HttpRequestException(SR.net_http_invalid_response_pseudo_header_in_trailer, httpRequestError: HttpRequestError.InvalidResponse);
                     }
 
                     Debug.Assert(_response != null);
@@ -681,7 +681,7 @@ namespace System.Net.Http
                     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 @@ namespace System.Net.Http
                     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 @@ namespace System.Net.Http
                     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);
index 0469cbc..828e0bb 100644 (file)
@@ -256,11 +256,14 @@ namespace System.Net.Http
 
                     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 @@ namespace System.Net.Http
                 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 @@ namespace System.Net.Http
                 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 @@ namespace System.Net.Http
                 {
                     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 @@ namespace System.Net.Http
                     {
                         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 @@ namespace System.Net.Http
                             {
                                 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 @@ namespace System.Net.Http
                     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 @@ namespace System.Net.Http
             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 @@ namespace System.Net.Http
                     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 @@ namespace System.Net.Http
             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(staticIndex: null, descriptor, staticValue: default, literalValue: value);
         }
@@ -1147,7 +1149,7 @@ namespace System.Net.Http
 
                         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 @@ namespace System.Net.Http
 
                         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 @@ namespace System.Net.Http
                 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 @@ namespace System.Net.Http
                     _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 @@ namespace System.Net.Http
             }
 
             _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<bool> ReadNextDataFrameAsync(HttpResponseMessage response, CancellationToken cancellationToken)
index a30aeb0..5fc58bf 100644 (file)
@@ -620,7 +620,7 @@ namespace System.Net.Http
                         _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 @@ namespace System.Net.Http
             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 @@ namespace System.Net.Http
                 }
                 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 @@ namespace System.Net.Http
             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 @@ namespace System.Net.Http
                     {
                         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 @@ namespace System.Net.Http
             }
 
             static void ThrowForInvalidHeaderLine(ReadOnlySpan<byte> 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<byte> name, ReadOnlySpan<byte> value, HttpResponseMessage response, bool isFromTrailer)
@@ -1281,14 +1281,14 @@ namespace System.Net.Http
             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<byte> 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 @@ namespace System.Net.Http
             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 @@ namespace System.Net.Http
 
             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 @@ namespace System.Net.Http
                 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);
                 }
             }
 
index d9bcb42..ed5091e 100644 (file)
@@ -164,7 +164,7 @@ namespace System.Net.Http
                 !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');
index d66d961..de967c4 100644 (file)
@@ -448,7 +448,7 @@ namespace System.Net.Http
         {
             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;
@@ -1095,7 +1095,7 @@ namespace System.Net.Http
                                     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;
                                     }
@@ -1162,7 +1162,7 @@ namespace System.Net.Http
                     // 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())
@@ -1775,7 +1775,7 @@ namespace System.Net.Http
             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
@@ -1802,7 +1802,7 @@ namespace System.Net.Http
             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;
index 224c116..f58b6a0 100644 (file)
@@ -32,13 +32,16 @@ namespace System.Net.Http.Functional.Tests
         {
             HttpRequestException outerEx = await Assert.ThrowsAsync<HttpRequestException>(() => task);
             _output.WriteLine(outerEx.InnerException.Message);
+            Assert.Equal(HttpRequestError.HttpProtocolError, outerEx.HttpRequestError);
             HttpProtocolException protocolEx = Assert.IsType<HttpProtocolException>(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<HttpProtocolException>(() => task);
+            Assert.Equal(HttpRequestError.HttpProtocolError, protocolEx.HttpRequestError);
             Assert.Equal(errorCode, (ProtocolErrors)protocolEx.ErrorCode);
         }
 
@@ -307,6 +310,22 @@ namespace System.Net.Http.Functional.Tests
         }
 
         [ConditionalFact(nameof(SupportsAlpn))]
+        public async Task Http2_IncorrectServerPreface_RequestFailsWithAppropriateHttpProtocolException()
+        {
+            using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer())
+            using (HttpClient client = CreateHttpClient())
+            {
+                Task<HttpResponseMessage> 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()
         {
             using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer())
index 10427c9..92c4ab0 100644 (file)
@@ -901,7 +901,7 @@ namespace System.Net.Http.Functional.Tests
                 }
                 else
                 {
-                    var ioe = Assert.IsType<IOException>(ex);
+                    var ioe = Assert.IsType<HttpIOException>(ex);
                     var hre = Assert.IsType<HttpRequestException>(ioe.InnerException);
                     var qex = Assert.IsType<QuicException>(hre.InnerException);
                     Assert.Equal(QuicError.OperationAborted, qex.QuicError);
index 207e7ec..0ea6ae9 100644 (file)
@@ -90,8 +90,7 @@ namespace System.Net.Http.Functional.Tests
                 request.Headers.Protocol = "foo";
 
                 HttpRequestException ex = await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(request));
-
-                Assert.Equal(false, ex.Data["SETTINGS_ENABLE_CONNECT_PROTOCOL"]);
+                Assert.Equal(HttpRequestError.ExtendedConnectNotSupported, ex.HttpRequestError);
 
                 clientCompleted.SetResult();
             },
@@ -156,7 +155,6 @@ namespace System.Net.Http.Functional.Tests
 
                 Exception ex = await Assert.ThrowsAnyAsync<Exception>(() => client.SendAsync(request));
                 clientCompleted.SetResult();
-
                 if (useSsl)
                 {
                     Assert.Equal(false, ex.Data["HTTP2_ENABLED"]);
index 9ec410e..e8784be 100644 (file)
@@ -4350,6 +4350,136 @@ namespace System.Net.Http.Functional.Tests
         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<HttpRequestException>(() => 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<HttpRequestException>(() => 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<HttpRequestException>(() => 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<HttpRequestException>(() => 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)
index 2e702c5..10221bf 100755 (executable)
              Link="ProductionCode\System\Net\Http\HttpCompletionOption.cs" />
     <Compile Include="..\..\src\System\Net\Http\HttpContent.cs"
              Link="ProductionCode\System\Net\Http\HttpContent.cs" />
+    <Compile Include="..\..\src\System\Net\Http\HttpIOException.cs"
+                 Link="ProductionCode\System\Net\Http\HttpIOException.cs" />
     <Compile Include="..\..\src\System\Net\Http\HttpMessageHandler.cs"
              Link="ProductionCode\System\Net\Http\HttpMessageHandler.cs" />
     <Compile Include="..\..\src\System\Net\Http\HttpMessageInvoker.cs"
              Link="ProductionCode\System\Net\Http\HttpMethod.cs" />
     <Compile Include="..\..\src\System\Net\Http\HttpParseResult.cs"
              Link="ProductionCode\System\Net\Http\HttpParseResult.cs" />
+    <Compile Include="..\..\src\System\Net\Http\HttpRequestError.cs"
+                 Link="ProductionCode\System\Net\Http\HttpRequestError.cs" />
     <Compile Include="..\..\src\System\Net\Http\HttpRequestException.cs"
              Link="ProductionCode\System\Net\Http\HttpRequestException.cs" />
     <Compile Include="..\..\src\System\Net\Http\RequestRetryType.cs"
index 786d1f9..3301bfe 100644 (file)
@@ -135,7 +135,7 @@ namespace System.Net.WebSockets
                         break;
                     }
                     catch (HttpRequestException ex) when
-                        ((ex.Data.Contains("SETTINGS_ENABLE_CONNECT_PROTOCOL") || ex.Data.Contains("HTTP2_ENABLED"))
+                        ((ex.HttpRequestError == HttpRequestError.ExtendedConnectNotSupported || ex.Data.Contains("HTTP2_ENABLED"))
                         && tryDowngrade
                         && (options.HttpVersion == HttpVersion.Version11 || options.HttpVersionPolicy == HttpVersionPolicy.RequestVersionOrLower))
                     {
index bdbbcb7..9de9b93 100644 (file)
@@ -75,7 +75,8 @@ namespace System.Net.WebSockets.Client.Tests
                     Task t = cws.ConnectAsync(uri, GetInvoker(), cts.Token);
 
                     var ex = await Assert.ThrowsAnyAsync<WebSocketException>(() => t);
-                    Assert.IsType<HttpRequestException>(ex.InnerException);
+                    HttpRequestException inner = Assert.IsType<HttpRequestException>(ex.InnerException);
+                    Assert.Equal(HttpRequestError.ExtendedConnectNotSupported, inner.HttpRequestError);
                     Assert.True(ex.InnerException.Data.Contains("SETTINGS_ENABLE_CONNECT_PROTOCOL"));
                 }
             },
@@ -100,7 +101,8 @@ namespace System.Net.WebSockets.Client.Tests
                     Task t = cws.ConnectAsync(uri, GetInvoker(), cts.Token);
 
                     var ex = await Assert.ThrowsAnyAsync<WebSocketException>(() => t);
-                    Assert.IsType<HttpRequestException>(ex.InnerException);
+                    HttpRequestException inner = Assert.IsType<HttpRequestException>(ex.InnerException);
+                    Assert.Equal(HttpRequestError.ExtendedConnectNotSupported, inner.HttpRequestError);
                     Assert.True(ex.InnerException.Data.Contains("SETTINGS_ENABLE_CONNECT_PROTOCOL"));
                 }
             },
@@ -124,8 +126,12 @@ namespace System.Net.WebSockets.Client.Tests
                 Task t = cws.ConnectAsync(Test.Common.Configuration.WebSockets.SecureRemoteEchoServer, GetInvoker(), cts.Token);
 
                 var ex = await Assert.ThrowsAnyAsync<WebSocketException>(() => t);
-                Assert.IsType<HttpRequestException>(ex.InnerException);
                 Assert.True(ex.InnerException.Data.Contains("HTTP2_ENABLED"));
+                HttpRequestException inner = Assert.IsType<HttpRequestException>(ex.InnerException);
+                HttpRequestError expectedError = PlatformDetection.SupportsAlpn ?
+                    HttpRequestError.SecureConnectionError :
+                    HttpRequestError.VersionNegotiationError;
+                Assert.Equal(expectedError, inner.HttpRequestError);
                 Assert.Equal(WebSocketState.Closed, cws.State);
             }
         }