Skip to content

Hang when HTTP/2 WebSocket connects to a HTTP/1.1 non-TLS kestrel endpoint #80056

@Tratcher

Description

@Tratcher

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.

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.

if (frameHeader.Type == FrameType.GoAway)
{
var (_, errorCode) = ReadGoAwayFrame(frameHeader);
ThrowProtocolError(errorCode, SR.net_http_http2_connection_close);
}

throw new IOException(SR.net_http_http2_connection_not_established, 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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions