Skip to content

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

Closed

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.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions