Skip to content

Commit 04bb7e5

Browse files
authored
Implement HttpRequestError (#88974)
Fixes #76644, fixes #82168.
1 parent dc6f9b4 commit 04bb7e5

27 files changed

+440
-104
lines changed

src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,16 @@ public async Task ReadAsStreamAsync_InvalidServerResponse_ThrowsIOException(
275275
{
276276
await StartTransferTypeAndErrorServer(transferType, transferError, async uri =>
277277
{
278-
await Assert.ThrowsAsync<IOException>(() => ReadAsStreamHelper(uri));
278+
if (IsWinHttpHandler)
279+
{
280+
await Assert.ThrowsAsync<IOException>(() => ReadAsStreamHelper(uri));
281+
}
282+
else
283+
{
284+
HttpIOException exception = await Assert.ThrowsAsync<HttpIOException>(() => ReadAsStreamHelper(uri));
285+
Assert.Equal(HttpRequestError.ResponseEnded, exception.HttpRequestError);
286+
}
287+
279288
});
280289
}
281290

src/libraries/System.Net.Http/ref/System.Net.Http.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,11 @@ protected virtual void SerializeToStream(System.IO.Stream stream, System.Net.Tra
203203
protected virtual System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext? context, System.Threading.CancellationToken cancellationToken) { throw null; }
204204
protected internal abstract bool TryComputeLength(out long length);
205205
}
206+
public class HttpIOException : System.IO.IOException
207+
{
208+
public System.Net.Http.HttpRequestError HttpRequestError { get { throw null; } }
209+
public HttpIOException(System.Net.Http.HttpRequestError httpRequestError, string? message = null, System.Exception? innerException = null) { }
210+
}
206211
public abstract partial class HttpMessageHandler : System.IDisposable
207212
{
208213
protected HttpMessageHandler() { }
@@ -241,17 +246,34 @@ public HttpMethod(string method) { }
241246
public static bool operator !=(System.Net.Http.HttpMethod? left, System.Net.Http.HttpMethod? right) { throw null; }
242247
public override string ToString() { throw null; }
243248
}
244-
public sealed class HttpProtocolException : System.IO.IOException
249+
public sealed class HttpProtocolException : System.Net.Http.HttpIOException
245250
{
246-
public HttpProtocolException(long errorCode, string? message, System.Exception? innerException) { }
251+
public HttpProtocolException(long errorCode, string? message, System.Exception? innerException) : base (default(System.Net.Http.HttpRequestError), default(string?), default(System.Exception?)) { }
247252
public long ErrorCode { get { throw null; } }
248253
}
254+
public enum HttpRequestError
255+
{
256+
Unknown = 0,
257+
NameResolutionError,
258+
ConnectionError,
259+
SecureConnectionError,
260+
HttpProtocolError,
261+
ExtendedConnectNotSupported,
262+
VersionNegotiationError,
263+
UserAuthenticationError,
264+
ProxyTunnelError,
265+
InvalidResponse,
266+
ResponseEnded,
267+
ConfigurationLimitExceeded,
268+
}
249269
public partial class HttpRequestException : System.Exception
250270
{
251271
public HttpRequestException() { }
252272
public HttpRequestException(string? message) { }
253273
public HttpRequestException(string? message, System.Exception? inner) { }
254274
public HttpRequestException(string? message, System.Exception? inner, System.Net.HttpStatusCode? statusCode) { }
275+
public HttpRequestException(string? message, System.Exception? inner = null, System.Net.HttpStatusCode? statusCode = null, System.Net.Http.HttpRequestError? httpRequestError = null) { }
276+
public System.Net.Http.HttpRequestError? HttpRequestError { get { throw null; } }
255277
public System.Net.HttpStatusCode? StatusCode { get { throw null; } }
256278
}
257279
public partial class HttpRequestMessage : System.IDisposable

src/libraries/System.Net.Http/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,9 @@
564564
<data name="net_http_proxy_tunnel_returned_failure_status_code" xml:space="preserve">
565565
<value>The proxy tunnel request to proxy '{0}' failed with status code '{1}'."</value>
566566
</data>
567+
<data name="net_http_proxy_tunnel_error" xml:space="preserve">
568+
<value>An error occurred while establishing a connection to the proxy tunnel.</value>
569+
</data>
567570
<data name="PlatformNotSupported_NetHttp" xml:space="preserve">
568571
<value>System.Net.Http is not supported on this platform.</value>
569572
</data>

src/libraries/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,12 @@
5858
<Compile Include="System\Net\Http\HttpParseResult.cs" />
5959
<Compile Include="System\Net\Http\HttpProtocolException.cs" />
6060
<Compile Include="System\Net\Http\HttpRequestException.cs" />
61+
<Compile Include="System\Net\Http\HttpRequestError.cs" />
6162
<Compile Include="System\Net\Http\HttpRequestMessage.cs" />
6263
<Compile Include="System\Net\Http\HttpRequestOptions.cs" />
6364
<Compile Include="System\Net\Http\HttpRequestOptionsKey.cs" />
6465
<Compile Include="System\Net\Http\HttpResponseMessage.cs" />
66+
<Compile Include="System\Net\Http\HttpIOException.cs" />
6567
<Compile Include="System\Net\Http\HttpRuleParser.cs" />
6668
<Compile Include="System\Net\Http\HttpTelemetry.cs" />
6769
<Compile Include="System\Net\Http\HttpVersionPolicy.cs" />

src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ private bool CreateTemporaryBuffer(long maxBufferSize, out MemoryStream? tempBuf
638638

639639
if (contentLength > maxBufferSize)
640640
{
641-
error = new HttpRequestException(SR.Format(System.Globalization.CultureInfo.InvariantCulture, SR.net_http_content_buffersize_exceeded, maxBufferSize));
641+
error = CreateOverCapacityException(maxBufferSize);
642642
return null;
643643
}
644644

@@ -719,7 +719,8 @@ private static Exception GetStreamCopyException(Exception originalException)
719719
internal static Exception WrapStreamCopyException(Exception e)
720720
{
721721
Debug.Assert(StreamCopyExceptionNeedsWrapping(e));
722-
return new HttpRequestException(SR.net_http_content_stream_copy_error, e);
722+
HttpRequestError error = e is HttpIOException ioEx ? ioEx.HttpRequestError : HttpRequestError.Unknown;
723+
return new HttpRequestException(SR.net_http_content_stream_copy_error, e, httpRequestError: error);
723724
}
724725

725726
private static int GetPreambleLength(ArraySegment<byte> buffer, Encoding encoding)
@@ -832,9 +833,9 @@ private static async Task<TResult> WaitAndReturnAsync<TState, TResult>(Task wait
832833
return returnFunc(state);
833834
}
834835

835-
private static HttpRequestException CreateOverCapacityException(int maxBufferSize)
836+
private static HttpRequestException CreateOverCapacityException(long maxBufferSize)
836837
{
837-
return new HttpRequestException(SR.Format(SR.net_http_content_buffersize_exceeded, maxBufferSize));
838+
return new HttpRequestException(SR.Format(System.Globalization.CultureInfo.InvariantCulture, SR.net_http_content_buffersize_exceeded, maxBufferSize), httpRequestError: HttpRequestError.ConfigurationLimitExceeded);
838839
}
839840

840841
internal sealed class LimitMemoryStream : MemoryStream
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO;
5+
6+
namespace System.Net.Http
7+
{
8+
/// <summary>
9+
/// An exception thrown when an error occurs while reading the response.
10+
/// </summary>
11+
public class HttpIOException : IOException
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="HttpIOException"/> class.
15+
/// </summary>
16+
/// <param name="httpRequestError">The <see cref="Http.HttpRequestError"/> that caused the exception.</param>
17+
/// <param name="message">The message string describing the error.</param>
18+
/// <param name="innerException">The exception that is the cause of the current exception.</param>
19+
public HttpIOException(HttpRequestError httpRequestError, string? message = null, Exception? innerException = null)
20+
: base(message, innerException)
21+
{
22+
HttpRequestError = httpRequestError;
23+
}
24+
25+
/// <summary>
26+
/// Gets the <see cref="Http.HttpRequestError"/> that caused the exception.
27+
/// </summary>
28+
public HttpRequestError HttpRequestError { get; }
29+
30+
/// <inheritdoc />
31+
public override string Message => $"{base.Message} ({HttpRequestError})";
32+
}
33+
}

src/libraries/System.Net.Http/src/System/Net/Http/HttpProtocolException.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.IO;
5+
using System.Net.Quic;
56

67
namespace System.Net.Http
78
{
@@ -14,7 +15,7 @@ namespace System.Net.Http
1415
/// When calling <see cref="Stream"/> methods on the stream returned by <see cref="HttpContent.ReadAsStream()"/> or
1516
/// <see cref="HttpContent.ReadAsStreamAsync(Threading.CancellationToken)"/>, <see cref="HttpProtocolException"/> can be thrown directly.
1617
/// </remarks>
17-
public sealed class HttpProtocolException : IOException
18+
public sealed class HttpProtocolException : HttpIOException
1819
{
1920
/// <summary>
2021
/// Initializes a new instance of the <see cref="HttpProtocolException"/> class with the specified error code,
@@ -24,7 +25,7 @@ public sealed class HttpProtocolException : IOException
2425
/// <param name="message">The error message that explains the reason for the exception.</param>
2526
/// <param name="innerException">The exception that is the cause of the current exception.</param>
2627
public HttpProtocolException(long errorCode, string message, Exception? innerException)
27-
: base(message, innerException)
28+
: base(Http.HttpRequestError.HttpProtocolError, message, innerException)
2829
{
2930
ErrorCode = errorCode;
3031
}
@@ -47,10 +48,10 @@ internal static HttpProtocolException CreateHttp2ConnectionException(Http2Protoc
4748
return new HttpProtocolException((long)protocolError, message, null);
4849
}
4950

50-
internal static HttpProtocolException CreateHttp3StreamException(Http3ErrorCode protocolError)
51+
internal static HttpProtocolException CreateHttp3StreamException(Http3ErrorCode protocolError, QuicException innerException)
5152
{
5253
string message = SR.Format(SR.net_http_http3_stream_error, GetName(protocolError), ((int)protocolError).ToString("x"));
53-
return new HttpProtocolException((long)protocolError, message, null);
54+
return new HttpProtocolException((long)protocolError, message, innerException);
5455
}
5556

5657
internal static HttpProtocolException CreateHttp3ConnectionException(Http3ErrorCode protocolError, string? message = null)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Net.Http
5+
{
6+
/// <summary>
7+
/// Defines error categories representing the reason for <see cref="HttpRequestException"/> or <see cref="HttpIOException"/>.
8+
/// </summary>
9+
public enum HttpRequestError
10+
{
11+
/// <summary>
12+
/// A generic or unknown error occurred.
13+
/// </summary>
14+
Unknown = 0,
15+
16+
/// <summary>
17+
/// The DNS name resolution failed.
18+
/// </summary>
19+
NameResolutionError,
20+
21+
/// <summary>
22+
/// A transport-level failure occurred while connecting to the remote endpoint.
23+
/// </summary>
24+
ConnectionError,
25+
26+
/// <summary>
27+
/// An error occurred during the TLS handshake.
28+
/// </summary>
29+
SecureConnectionError,
30+
31+
/// <summary>
32+
/// An HTTP/2 or HTTP/3 protocol error occurred.
33+
/// </summary>
34+
HttpProtocolError,
35+
36+
/// <summary>
37+
/// Extended CONNECT for WebSockets over HTTP/2 is not supported by the peer.
38+
/// </summary>
39+
ExtendedConnectNotSupported,
40+
41+
/// <summary>
42+
/// Cannot negotiate the HTTP Version requested.
43+
/// </summary>
44+
VersionNegotiationError,
45+
46+
/// <summary>
47+
/// The authentication failed.
48+
/// </summary>
49+
UserAuthenticationError,
50+
51+
/// <summary>
52+
/// An error occurred while establishing a connection to the proxy tunnel.
53+
/// </summary>
54+
ProxyTunnelError,
55+
56+
/// <summary>
57+
/// An invalid or malformed response has been received.
58+
/// </summary>
59+
InvalidResponse,
60+
61+
/// <summary>
62+
/// The response ended prematurely.
63+
/// </summary>
64+
ResponseEnded,
65+
66+
/// <summary>
67+
/// The response exceeded a pre-configured limit such as <see cref="HttpClient.MaxResponseContentBufferSize"/> or <see cref="HttpClientHandler.MaxResponseHeadersLength"/>.
68+
/// </summary>
69+
ConfigurationLimitExceeded,
70+
}
71+
}

src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestException.cs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Diagnostics.CodeAnalysis;
5-
using System.IO;
6-
74
namespace System.Net.Http
85
{
96
public class HttpRequestException : Exception
107
{
118
internal RequestRetryType AllowRetry { get; } = RequestRetryType.NoRetry;
129

1310
public HttpRequestException()
14-
: this(null, null)
1511
{ }
1612

1713
public HttpRequestException(string? message)
18-
: this(message, null)
14+
: base(message)
1915
{ }
2016

2117
public HttpRequestException(string? message, Exception? inner)
@@ -39,6 +35,27 @@ public HttpRequestException(string? message, Exception? inner, HttpStatusCode? s
3935
StatusCode = statusCode;
4036
}
4137

38+
/// <summary>
39+
/// 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"/>.
40+
/// </summary>
41+
/// <param name="message">A message that describes the current exception.</param>
42+
/// <param name="inner">The inner exception.</param>
43+
/// <param name="statusCode">The HTTP status code.</param>
44+
/// <param name="httpRequestError">The <see cref="HttpRequestError"/> that caused the exception.</param>
45+
public HttpRequestException(string? message, Exception? inner = null, HttpStatusCode? statusCode = null, HttpRequestError? httpRequestError = null)
46+
: this(message, inner, statusCode)
47+
{
48+
HttpRequestError = httpRequestError;
49+
}
50+
51+
/// <summary>
52+
/// Gets the <see cref="Http.HttpRequestError"/> that caused the exception.
53+
/// </summary>
54+
/// <value>
55+
/// The <see cref="Http.HttpRequestError"/> or <see langword="null"/> if the underlying <see cref="HttpMessageHandler"/> did not provide it.
56+
/// </value>
57+
public HttpRequestError? HttpRequestError { get; }
58+
4259
/// <summary>
4360
/// Gets the HTTP status code to be returned with the exception.
4461
/// </summary>
@@ -49,8 +66,8 @@ public HttpRequestException(string? message, Exception? inner, HttpStatusCode? s
4966

5067
// This constructor is used internally to indicate that a request was not successfully sent due to an IOException,
5168
// and the exception occurred early enough so that the request may be retried on another connection.
52-
internal HttpRequestException(string? message, Exception? inner, RequestRetryType allowRetry)
53-
: this(message, inner)
69+
internal HttpRequestException(string? message, Exception? inner, RequestRetryType allowRetry, HttpRequestError? httpRequestError = null)
70+
: this(message, inner, httpRequestError: httpRequestError)
5471
{
5572
AllowRetry = allowRetry;
5673
}

src/libraries/System.Net.Http/src/System/Net/Http/Metrics/HttpMetricsEnrichmentContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public sealed class HttpMetricsEnrichmentContext
5353
public HttpResponseMessage? Response => _response;
5454

5555
/// <summary>
56-
/// Gets the exception that occured or <see langword="null"/> if there was no error.
56+
/// Gets the exception that occurred or <see langword="null"/> if there was no error.
5757
/// </summary>
5858
/// <remarks>
5959
/// This property must not be used from outside of the enrichment callbacks.

0 commit comments

Comments
 (0)