WebSockets over HTTP/2 issues
BrennanConroy opened this issue · 6 comments
Description
The new WebSockets over HTTP/2 feature doesn't work with ASP.NET Core 7.
I believe it is because this line
Sepcifically:
endStream: (request.Content == null)
Causes the END_STREAM flag to be set when sending headers for the CONNECT request.
dbug: Microsoft.AspNetCore.Server.Kestrel.Core.Internal.LoggingConnectionMiddleware[0]
ReadAsync[83]
00 00 4A 01 05 00 00 00 01 02 07 43 4F 4E 4E 45 ..J..... ...CONNE
43 54 87 01 0E 6C 6F 63 61 6C 68 6F 73 74 3A 37 CT...loc alhost:7
32 30 34 84 00 09 3A 70 72 6F 74 6F 63 6F 6C 09 204...:p rotocol.
77 65 62 73 6F 63 6B 65 74 00 15 73 65 63 2D 77 websocke t..sec-w
65 62 73 6F 63 6B 65 74 2D 76 65 72 73 69 6F 6E ebsocket -version
02 31 33 .13
trce: Microsoft.AspNetCore.Server.Kestrel.Http2[37]
Connection id "0HMJ6N3UAOPRE" received HEADERS frame for stream ID 1 with length 74 and flags END_STREAM, END_HEADERS.
Additional strange behavior, if you use the ClientWebSocket
without passing in an HttpClientHandler
then the ConnectAsync
call does not throw, but the websocket is closed on the server side.
Reproduction Steps
Client project
var webSocket = new ClientWebSocket();
webSocket.Options.HttpVersion = HttpVersion.Version20;
var httpClientHandler = new HttpClientHandler();
await webSocket.ConnectAsync(url, new HttpClient(httpClientHandler), default).ConfigureAwait(false);
Server project
var builder = WebApplication.CreateBuilder(args);
builder.Logging.SetMinimumLevel(LogLevel.Trace);
builder.WebHost.ConfigureKestrel(o =>
{
o.ConfigureEndpointDefaults(l =>
{
l.UseHttps();
l.UseConnectionLogging();
});
});
var app = builder.Build();
app.UseWebSockets();
app.Run(async context =>
{
var websocket = await context.WebSockets.AcceptWebSocketAsync();
var buf = await websocket.ReceiveAsync(Memory<byte>.Empty, default);
});
app.Run();
Expected behavior
Able to connect to a WebSocket endpoint over HTTP/2.
Actual behavior
Unhandled exception. System.AggregateException: One or more errors occurred. (Unable to connect to the remote server)
---> System.Net.WebSockets.WebSocketException (0x80004005): Unable to connect to the remote server
---> System.Net.Http.HttpRequestException: Error while copying content to a stream.
---> System.Net.Http.HttpProtocolException: The HTTP/2 server reset the stream. HTTP/2 error code 'INTERNAL_ERROR' (0x2).
at System.Net.Http.Http2Connection.ThrowRequestAborted(Exception innerException)
at System.Net.Http.Http2Connection.Http2Stream.CheckResponseBodyState()
at System.Net.Http.Http2Connection.Http2Stream.TryReadFromBuffer(Span`1 buffer, Boolean partOfSyncRead)
at System.Net.Http.Http2Connection.Http2Stream.CopyToAsync(HttpResponseMessage responseMessage, Stream destination, Int32 bufferSize, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionResponseContent.<SerializeToStreamAsync>g__Impl|6_0(Stream stream, TransportContext context, CancellationToken cancellationToken)
at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
--- End of inner exception stack trace ---
at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, HttpMessageInvoker invoker, CancellationToken cancellationToken, ClientWebSocketOptions options)
at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, HttpMessageInvoker invoker, CancellationToken cancellationToken, ClientWebSocketOptions options)
at System.Net.WebSockets.ClientWebSocket.ConnectAsyncCore(Uri uri, HttpMessageInvoker invoker, CancellationToken cancellationToken)
Regression?
No
Known Workarounds
None
Configuration
No response
Other information
No response
Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.
Issue Details
Description
The new WebSockets over HTTP/2 feature doesn't work with ASP.NET Core 7.
I believe it is because this line
Sepcifically:
endStream: (request.Content == null)
Causes the END_STREAM flag to be set when sending headers for the CONNECT request.
dbug: Microsoft.AspNetCore.Server.Kestrel.Core.Internal.LoggingConnectionMiddleware[0]
ReadAsync[83]
00 00 4A 01 05 00 00 00 01 02 07 43 4F 4E 4E 45 ..J..... ...CONNE
43 54 87 01 0E 6C 6F 63 61 6C 68 6F 73 74 3A 37 CT...loc alhost:7
32 30 34 84 00 09 3A 70 72 6F 74 6F 63 6F 6C 09 204...:p rotocol.
77 65 62 73 6F 63 6B 65 74 00 15 73 65 63 2D 77 websocke t..sec-w
65 62 73 6F 63 6B 65 74 2D 76 65 72 73 69 6F 6E ebsocket -version
02 31 33 .13
trce: Microsoft.AspNetCore.Server.Kestrel.Http2[37]
Connection id "0HMJ6N3UAOPRE" received HEADERS frame for stream ID 1 with length 74 and flags END_STREAM, END_HEADERS.
Additional strange behavior, if you use the ClientWebSocket
without passing in an HttpClientHandler
then the ConnectAsync
call does not throw, but the websocket is closed on the server side.
Reproduction Steps
Client project
var webSocket = new ClientWebSocket();
webSocket.Options.HttpVersion = HttpVersion.Version20;
var httpClientHandler = new HttpClientHandler();
await webSocket.ConnectAsync(url, new HttpClient(httpClientHandler), default).ConfigureAwait(false);
Server project
var builder = WebApplication.CreateBuilder(args);
builder.Logging.SetMinimumLevel(LogLevel.Trace);
builder.WebHost.ConfigureKestrel(o =>
{
o.ConfigureEndpointDefaults(l =>
{
l.UseHttps();
l.UseConnectionLogging();
});
});
var app = builder.Build();
app.UseWebSockets();
app.Run(async context =>
{
var websocket = await context.WebSockets.AcceptWebSocketAsync();
var buf = await websocket.ReceiveAsync(Memory<byte>.Empty, default);
});
app.Run();
Expected behavior
Able to connect to a WebSocket endpoint over HTTP/2.
Actual behavior
Unhandled exception. System.AggregateException: One or more errors occurred. (Unable to connect to the remote server)
---> System.Net.WebSockets.WebSocketException (0x80004005): Unable to connect to the remote server
---> System.Net.Http.HttpRequestException: Error while copying content to a stream.
---> System.Net.Http.HttpProtocolException: The HTTP/2 server reset the stream. HTTP/2 error code 'INTERNAL_ERROR' (0x2).
at System.Net.Http.Http2Connection.ThrowRequestAborted(Exception innerException)
at System.Net.Http.Http2Connection.Http2Stream.CheckResponseBodyState()
at System.Net.Http.Http2Connection.Http2Stream.TryReadFromBuffer(Span`1 buffer, Boolean partOfSyncRead)
at System.Net.Http.Http2Connection.Http2Stream.CopyToAsync(HttpResponseMessage responseMessage, Stream destination, Int32 bufferSize, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionResponseContent.<SerializeToStreamAsync>g__Impl|6_0(Stream stream, TransportContext context, CancellationToken cancellationToken)
at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
--- End of inner exception stack trace ---
at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, HttpMessageInvoker invoker, CancellationToken cancellationToken, ClientWebSocketOptions options)
at System.Net.WebSockets.WebSocketHandle.ConnectAsync(Uri uri, HttpMessageInvoker invoker, CancellationToken cancellationToken, ClientWebSocketOptions options)
at System.Net.WebSockets.ClientWebSocket.ConnectAsyncCore(Uri uri, HttpMessageInvoker invoker, CancellationToken cancellationToken)
Regression?
No
Known Workarounds
None
Configuration
No response
Other information
No response
Author: | BrennanConroy |
---|---|
Assignees: | - |
Labels: |
|
Milestone: | - |
Triage: Incorrect sending of final flag. We should fix it.
I tested the same scenario with HttpMessageInvoker and it works, it seems that using HttpClient is the root cause as in #72476
@BrennanConroy you mentioned that it failed without handler parameter, could you please provide more details? I cannot reproduce it - connect and send-receive with local server work for me. Is it the same setup, with or without TLS?
I tested the same scenario with HttpMessageInvoker and it works
It works because it falls back to HTTP/1.1 (i.e. it fails HTTP/2.0 which is what this issue is about).
So set webSocket.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;
to see the failure.
Edit: Passing an HttpMessageInvoker
has the same issue as not passing a handler, the server closes the websocket but the client "sends" perfectly fine. What local server are you using?
you mentioned that it failed without handler parameter, could you please provide more details?
Tried again and not seeing any issues in this case (besides throwing for HTTP/2.0).
Edit: Whoops, wrong server version. Tried again and see the issue.
var webSocket = new ClientWebSocket();
webSocket.Options.HttpVersion = HttpVersion.Version20;
await webSocket.ConnectAsync(new Uri("wss://localhost:7204"), default).ConfigureAwait(false);
// this call "succeeds" but the server has closed the websocket connection due to the previous END_STREAM flag
await webSocket.SendAsync(new byte[] { 35, 36, 37 }, WebSocketMessageType.Binary, true, default);
// throws
var res = await webSocket.ReceiveAsync(Array.Empty<byte>(), default);
What local server are you using?
Kestrel 7.0.0-rc.1.22368.6
Ah, I see it now. It is strange that switching the order first receive and than send work
Reopening as the issue does not seem fixed, see #73222 (comment)