-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Description
When using HTTP/2 without TLS it's a common mistake to connect to a HTTP/1.1 endpoint. Kestrel added special handling for this scenario by responding with a HTTP/2 GoAway HTTP_1_1_REQUIRED. Normally this works with HttpClient, but in the new HTTP/2 WebSocket scenario it hangs and times out instead.
From the stack traces, the issue appears to be here. It's stuck waiting for a settings frame that was never received.
runtime/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Lines 1068 to 1077 in c84d95d
| if (request.IsExtendedConnectRequest) | |
| { | |
| await connection.InitialSettingsReceived.WaitWithCancellationAsync(cancellationToken).ConfigureAwait(false); | |
| if (!connection.IsConnectEnabled) | |
| { | |
| HttpRequestException exception = new(SR.net_unsupported_extended_connect); | |
| exception.Data["SETTINGS_ENABLE_CONNECT_PROTOCOL"] = false; | |
| throw exception; | |
| } | |
| } |
InitialSettingsReceived needs to complete/fail if the connection is aborted.
runtime/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
Lines 500 to 504 in c84d95d
| if (frameHeader.Type == FrameType.GoAway) | |
| { | |
| var (_, errorCode) = ReadGoAwayFrame(frameHeader); | |
| ThrowProtocolError(errorCode, SR.net_http_http2_connection_close); | |
| } |
runtime/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
Line 518 in c84d95d
| throw new IOException(SR.net_http_http2_connection_not_established, e); |
runtime/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
Line 603 in c84d95d
| Abort(e); |
Reproduction Steps
using System.Net;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Urls.Add("http://localhost:5000");
app.MapGet("/", () => "Hello World!");
app.Start();
using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Connect, "http://localhost:5000");
request.Headers.Protocol = "websocket"; // Trigger, comment out to avoid issue
request.Version = HttpVersion.Version20;
request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
Console.WriteLine(response);
Console.WriteLine(await response.Content.ReadAsStringAsync());Expected behavior
We should get back the following error. This is what you get if you comment out the line request.Headers.Protocol = "websocket";
Unhandled exception. System.Net.Http.HttpRequestException: An error occurred while sending the request.
---> System.IO.IOException: The request was aborted.
---> System.IO.IOException: An HTTP/2 connection could not be established because the server did not complete the HTTP/2 handshake.
---> System.Net.Http.HttpProtocolException: The HTTP/2 server closed the connection. HTTP/2 error code 'HTTP_1_1_REQUIRED' (0xd).
at System.Net.Http.Http2Connection.ThrowProtocolError(Http2ProtocolErrorCode errorCode, String message)
at System.Net.Http.Http2Connection.ProcessIncomingFramesAsync()
--- End of inner exception stack trace ---
at System.Net.Http.Http2Connection.ProcessIncomingFramesAsync()
--- End of inner exception stack trace ---
at System.Net.Http.Http2Connection.ThrowRequestAborted(Exception innerException)
at System.Net.Http.Http2Connection.Http2Stream.CheckResponseBodyState()
at System.Net.Http.Http2Connection.Http2Stream.TryEnsureHeaders()
at System.Net.Http.Http2Connection.Http2Stream.ReadResponseHeadersAsync(CancellationToken cancellationToken)
at System.Net.Http.Http2Connection.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at System.Net.Http.Http2Connection.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at Program.<Main>$(String[] args) in C:\temp\WebApplication126\WebApplication126\Program.cs:line 14
at Program.<Main>(String[] args)
Actual behavior
It hangs and eventually times out
Unhandled exception. System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.
---> System.TimeoutException: A task was canceled.
---> System.Threading.Tasks.TaskCanceledException: A task was canceled.
at System.Threading.Tasks.TaskCompletionSourceWithCancellation`1.WaitWithCancellationAsync(CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
--- End of inner exception stack trace ---
--- End of inner exception stack trace ---
at System.Net.Http.HttpClient.HandleFailure(Exception e, Boolean telemetryStarted, HttpResponseMessage response, CancellationTokenSource cts, CancellationToken cancellationToken, CancellationTokenSource pendingRequestsCts)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at Program.<Main>$(String[] args) in C:\temp\WebApplication126\WebApplication126\Program.cs:line 17
at Program.<Main>(String[] args)
Regression?
No response
Known Workarounds
No response
Configuration
.NET 7
Other information
The scenario isn't supposed to succeed, but now it fails in a way that's hard to debug.