Skip to content

Commit f3a04a6

Browse files
authored
Implement HttpProtocolException for HTTP/2 (#71345)
Contributes to #70684
1 parent ce6d3df commit f3a04a6

File tree

8 files changed

+98
-130
lines changed

8 files changed

+98
-130
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ public HttpMethod(string method) { }
238238
public static bool operator !=(System.Net.Http.HttpMethod? left, System.Net.Http.HttpMethod? right) { throw null; }
239239
public override string ToString() { throw null; }
240240
}
241+
public sealed class HttpProtocolException : System.IO.IOException
242+
{
243+
public HttpProtocolException(long errorCode, string? message, System.Exception? innerException) { }
244+
public long ErrorCode { get { throw null; } }
245+
}
241246
public partial class HttpRequestException : System.Exception
242247
{
243248
public HttpRequestException() { }

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
<Compile Include="System\Net\Http\HttpMessageInvoker.cs" />
5757
<Compile Include="System\Net\Http\HttpMethod.cs" />
5858
<Compile Include="System\Net\Http\HttpParseResult.cs" />
59+
<Compile Include="System\Net\Http\HttpProtocolException.cs" />
5960
<Compile Include="System\Net\Http\HttpRequestException.cs" />
6061
<Compile Include="System\Net\Http\HttpRequestMessage.cs" />
6162
<Compile Include="System\Net\Http\HttpRequestOptions.cs" />
@@ -175,11 +176,8 @@
175176
<Compile Include="System\Net\Http\SocketsHttpHandler\DecompressionHandler.cs" />
176177
<Compile Include="System\Net\Http\SocketsHttpHandler\FailedProxyCache.cs" />
177178
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2Connection.cs" />
178-
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2ConnectionException.cs" />
179179
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2ProtocolErrorCode.cs" />
180-
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2ProtocolException.cs" />
181180
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2Stream.cs" />
182-
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2StreamException.cs" />
183181
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2StreamWindowManager.cs" />
184182
<Compile Include="System\Net\Http\SocketsHttpHandler\Http3Connection.cs" />
185183
<Compile Include="System\Net\Http\SocketsHttpHandler\Http3ConnectionException.cs" />
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
/// The exception thrown when an HTTP/2 or an HTTP/3 protocol error occurs.
10+
/// </summary>
11+
/// <remarks>
12+
/// When calling <see cref="HttpClient"/> or <see cref="SocketsHttpHandler"/> methods, <see cref="HttpProtocolException"/> will be the inner exception of
13+
/// <see cref="HttpRequestException"/> if a protocol error occurs.
14+
/// When calling <see cref="Stream"/> methods on the stream returned by <see cref="HttpContent.ReadAsStream()"/> or
15+
/// <see cref="HttpContent.ReadAsStreamAsync(Threading.CancellationToken)"/>, <see cref="HttpProtocolException"/> can be thrown directly.
16+
/// </remarks>
17+
public sealed class HttpProtocolException : IOException
18+
{
19+
/// <summary>
20+
/// Initializes a new instance of the <see cref="HttpProtocolException"/> class with the specified error code,
21+
/// message, and inner exception.
22+
/// </summary>
23+
/// <param name="errorCode">The HTTP/2 or HTTP/3 error code.</param>
24+
/// <param name="message">The error message that explains the reason for the exception.</param>
25+
/// <param name="innerException">The exception that is the cause of the current exception.</param>
26+
public HttpProtocolException(long errorCode, string message, Exception? innerException)
27+
: base(message, innerException)
28+
{
29+
ErrorCode = errorCode;
30+
}
31+
32+
/// <summary>
33+
/// Gets the HTTP/2 or HTTP/3 error code associated with this exception.
34+
/// </summary>
35+
public long ErrorCode { get; }
36+
37+
#if !TARGET_BROWSER
38+
internal static HttpProtocolException CreateHttp2StreamException(Http2ProtocolErrorCode protocolError)
39+
{
40+
string message = SR.Format(SR.net_http_http2_stream_error, GetName(protocolError), ((int)protocolError).ToString("x"));
41+
return new HttpProtocolException((long)protocolError, message, null);
42+
}
43+
44+
internal static HttpProtocolException CreateHttp2ConnectionException(Http2ProtocolErrorCode protocolError)
45+
{
46+
string message = SR.Format(SR.net_http_http2_connection_error, GetName(protocolError), ((int)protocolError).ToString("x"));
47+
return new HttpProtocolException((long)protocolError, message, null);
48+
}
49+
50+
private static string GetName(Http2ProtocolErrorCode code) =>
51+
// These strings are the names used in the HTTP2 spec and should not be localized.
52+
code switch
53+
{
54+
Http2ProtocolErrorCode.NoError => "NO_ERROR",
55+
Http2ProtocolErrorCode.ProtocolError => "PROTOCOL_ERROR",
56+
Http2ProtocolErrorCode.InternalError => "INTERNAL_ERROR",
57+
Http2ProtocolErrorCode.FlowControlError => "FLOW_CONTROL_ERROR",
58+
Http2ProtocolErrorCode.SettingsTimeout => "SETTINGS_TIMEOUT",
59+
Http2ProtocolErrorCode.StreamClosed => "STREAM_CLOSED",
60+
Http2ProtocolErrorCode.FrameSizeError => "FRAME_SIZE_ERROR",
61+
Http2ProtocolErrorCode.RefusedStream => "REFUSED_STREAM",
62+
Http2ProtocolErrorCode.Cancel => "CANCEL",
63+
Http2ProtocolErrorCode.CompressionError => "COMPRESSION_ERROR",
64+
Http2ProtocolErrorCode.ConnectError => "CONNECT_ERROR",
65+
Http2ProtocolErrorCode.EnhanceYourCalm => "ENHANCE_YOUR_CALM",
66+
Http2ProtocolErrorCode.InadequateSecurity => "INADEQUATE_SECURITY",
67+
Http2ProtocolErrorCode.Http11Required => "HTTP_1_1_REQUIRED",
68+
_ => "(unknown error)",
69+
};
70+
#endif
71+
}
72+
}

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -993,14 +993,8 @@ private void ProcessRstStreamFrame(FrameHeader frameHeader)
993993

994994
_incomingBuffer.Discard(frameHeader.PayloadLength);
995995

996-
if (protocolError == Http2ProtocolErrorCode.RefusedStream)
997-
{
998-
http2Stream.OnReset(new Http2StreamException(protocolError), resetStreamErrorCode: protocolError, canRetry: true);
999-
}
1000-
else
1001-
{
1002-
http2Stream.OnReset(new Http2StreamException(protocolError), resetStreamErrorCode: protocolError);
1003-
}
996+
bool canRetry = protocolError == Http2ProtocolErrorCode.RefusedStream;
997+
http2Stream.OnReset(HttpProtocolException.CreateHttp2StreamException(protocolError), resetStreamErrorCode: protocolError, canRetry: canRetry);
1004998
}
1005999

10061000
private void ProcessGoAwayFrame(FrameHeader frameHeader)
@@ -1024,7 +1018,7 @@ private void ProcessGoAwayFrame(FrameHeader frameHeader)
10241018
_incomingBuffer.Discard(frameHeader.PayloadLength);
10251019

10261020
Debug.Assert(lastStreamId >= 0);
1027-
Exception resetException = new Http2ConnectionException(errorCode);
1021+
Exception resetException = HttpProtocolException.CreateHttp2ConnectionException(errorCode);
10281022

10291023
// There is no point sending more PING frames for RTT estimation:
10301024
_rttEstimator.OnGoAwayReceived();
@@ -1964,7 +1958,7 @@ await Task.WhenAny(requestBodyTask, responseHeadersTask).ConfigureAwait(false) =
19641958
{
19651959
if (e is IOException ||
19661960
e is ObjectDisposedException ||
1967-
e is Http2ProtocolException ||
1961+
e is HttpProtocolException ||
19681962
e is InvalidOperationException)
19691963
{
19701964
throw new HttpRequestException(SR.net_http_client_execution_error, e);
@@ -2070,7 +2064,7 @@ private static void ThrowRetry(string message, Exception? innerException = null)
20702064
throw new HttpRequestException(message, innerException, allowRetry: RequestRetryType.RetryOnConnectionFailure);
20712065

20722066
private static Exception GetRequestAbortedException(Exception? innerException = null) =>
2073-
new IOException(SR.net_http_request_aborted, innerException);
2067+
innerException as HttpProtocolException ?? new IOException(SR.net_http_request_aborted, innerException);
20742068

20752069
[DoesNotReturn]
20762070
private static void ThrowRequestAborted(Exception? innerException = null) =>
@@ -2082,6 +2076,6 @@ private static void ThrowProtocolError() =>
20822076

20832077
[DoesNotReturn]
20842078
private static void ThrowProtocolError(Http2ProtocolErrorCode errorCode) =>
2085-
throw new Http2ConnectionException(errorCode);
2079+
throw HttpProtocolException.CreateHttp2ConnectionException(errorCode);
20862080
}
20872081
}

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2ConnectionException.cs

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2ProtocolException.cs

Lines changed: 0 additions & 51 deletions
This file was deleted.

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2StreamException.cs

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,18 @@ public HttpClientHandlerTest_Http2(ITestOutputHelper output) : base(output)
2828
{
2929
}
3030

31-
private async Task AssertProtocolErrorAsync<T>(Task task, ProtocolErrors errorCode)
32-
where T : Exception
31+
private async Task AssertProtocolErrorAsync(Task task, ProtocolErrors errorCode)
3332
{
34-
Exception e = await Assert.ThrowsAsync<T>(() => task);
35-
string text = e.ToString();
36-
Assert.Contains(((int)errorCode).ToString("x"), text);
37-
Assert.Contains(
38-
Enum.IsDefined(typeof(ProtocolErrors), errorCode) ? errorCode.ToString() : "(unknown error)",
39-
text);
33+
HttpRequestException outerEx = await Assert.ThrowsAsync<HttpRequestException>(() => task);
34+
_output.WriteLine(outerEx.InnerException.Message);
35+
HttpProtocolException protocolEx = Assert.IsType<HttpProtocolException>(outerEx.InnerException);
36+
Assert.Equal(errorCode, (ProtocolErrors)protocolEx.ErrorCode);
4037
}
4138

42-
private Task AssertProtocolErrorAsync(Task task, ProtocolErrors errorCode)
39+
private async Task AssertProtocolErrorForIOExceptionAsync(Task task, ProtocolErrors errorCode)
4340
{
44-
return AssertProtocolErrorAsync<HttpRequestException>(task, errorCode);
45-
}
46-
47-
private Task AssertProtocolErrorForIOExceptionAsync(Task task, ProtocolErrors errorCode)
48-
{
49-
return AssertProtocolErrorAsync<IOException>(task, errorCode);
41+
HttpProtocolException protocolEx = await Assert.ThrowsAsync<HttpProtocolException>(() => task);
42+
Assert.Equal(errorCode, (ProtocolErrors)protocolEx.ErrorCode);
5043
}
5144

5245
private async Task<(bool, T)> IgnoreSpecificException<ExpectedException, T>(Task<T> task, string expectedExceptionContent = null) where ExpectedException : Exception
@@ -959,9 +952,8 @@ public async Task GoAwayFrame_RequestWithBody_ServerDisconnect_AbortStreamsAndTh
959952
sendTask
960953
}.WhenAllOrAnyFailed(TestHelper.PassingTestTimeoutMilliseconds));
961954

962-
Assert.IsType<IOException>(exception.InnerException);
963-
Assert.NotNull(exception.InnerException.InnerException);
964-
Assert.Contains("PROTOCOL_ERROR", exception.InnerException.InnerException.Message);
955+
var protocolException = Assert.IsType<HttpProtocolException>(exception.InnerException);
956+
Assert.Equal((long)ProtocolErrors.PROTOCOL_ERROR, protocolException.ErrorCode);
965957
}
966958
}
967959

@@ -3441,7 +3433,7 @@ await Http2LoopbackServer.CreateClientAndServerAsync(
34413433
// An exception will be thrown by either GetAsync or ReadAsStringAsync once
34423434
// the inbound window size has been exceeded. Which one depends on how quickly
34433435
// ProcessIncomingFramesAsync() can read data off the socket.
3444-
Exception requestException = await Assert.ThrowsAsync<HttpRequestException>(async () =>
3436+
HttpRequestException requestException = await Assert.ThrowsAsync<HttpRequestException>(async () =>
34453437
{
34463438
using HttpClient client = CreateHttpClient();
34473439
using HttpResponseMessage response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
@@ -3452,15 +3444,9 @@ await Http2LoopbackServer.CreateClientAndServerAsync(
34523444
await response.Content.ReadAsStringAsync();
34533445
});
34543446

3455-
// A Http2ConnectionException will be present somewhere in the inner exceptions.
3456-
// Its location depends on which method threw the exception.
3457-
while (requestException?.GetType().FullName.Equals("System.Net.Http.Http2ConnectionException") == false)
3458-
{
3459-
requestException = requestException.InnerException;
3460-
}
3461-
3462-
Assert.NotNull(requestException);
3463-
Assert.Contains("FLOW_CONTROL_ERROR", requestException.Message);
3447+
HttpProtocolException protocolException = Assert.IsType<HttpProtocolException>(requestException.InnerException);
3448+
Assert.Equal((long)ProtocolErrors.FLOW_CONTROL_ERROR, protocolException.ErrorCode);
3449+
Assert.Contains("FLOW_CONTROL_ERROR", protocolException.Message);
34643450
},
34653451
async server =>
34663452
{

0 commit comments

Comments
 (0)