Skip to content

Commit

Permalink
Implement HttpRequestError (#88974)
Browse files Browse the repository at this point in the history
Fixes #76644, fixes #82168.
  • Loading branch information
antonfirsov authored Jul 18, 2023
1 parent dc6f9b4 commit 04bb7e5
Show file tree
Hide file tree
Showing 27 changed files with 440 additions and 104 deletions.
11 changes: 10 additions & 1 deletion src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,16 @@ public async Task ReadAsStreamAsync_InvalidServerResponse_ThrowsIOException(
{
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);
}
});
}

Expand Down
26 changes: 24 additions & 2 deletions src/libraries/System.Net.Http/ref/System.Net.Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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() { }
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/System.Net.Http/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,9 @@
<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>
Expand Down
2 changes: 2 additions & 0 deletions src/libraries/System.Net.Http/src/System.Net.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@
<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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<byte> buffer, Encoding encoding)
Expand Down Expand Up @@ -832,9 +833,9 @@ private static async Task<TResult> WaitAndReturnAsync<TState, TResult>(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
Expand Down
Original file line number Diff line number Diff line change
@@ -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})";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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,
Expand All @@ -24,7 +25,7 @@ public sealed class HttpProtocolException : IOException
/// <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;
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
// 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
{
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)
Expand All @@ -39,6 +35,27 @@ public HttpRequestException(string? message, Exception? inner, HttpStatusCode? s
StatusCode = statusCode;
}

/// <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>
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public sealed class HttpMetricsEnrichmentContext
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ private static async Task<HttpResponseMessage> 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;
}
Expand Down
Loading

0 comments on commit 04bb7e5

Please sign in to comment.