Repository navigation
WebSockets over HTTP/1.1 with HttpClient fails #72476
Copy link
Copy link
Closed
Description
Description
Similar to #72301, but instead of setting the version to HTTP/2.0 when using ClientWebSocket (or when it falls back to 1.1 if 2.0 is unsuccessful/not supported), the websocket request is made to the server but fails after the HttpClient.Timeout period because it tries to read the entire response.
Code the client is stuck on
| if (ShouldBufferResponse(completionOption, request)) |
The issue is that
HttpClient sets HttpCompletionOption.ResponseContentRead when SendAsync is called via the HttpMessageInvoker abstraction, when we likely want HttpCompletionOption.ResponseHeadersRead instead.| SendAsync(request, DefaultCompletionOption, cancellationToken); |
Reproduction Steps
Client project
var webSocket = new ClientWebSocket();
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
WebSocket connects over HTTP/1.1 successfully.
Actual behavior
Client-side exception:
Details
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.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.
---> System.TimeoutException: The operation was canceled.
---> System.OperationCanceledException: The operation was canceled.
at System.Threading.CancellationToken.ThrowOperationCanceledException()
at System.Threading.CancellationToken.ThrowIfCancellationRequested()
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken)
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource<System.Int32>.GetResult(Int16 token)
at System.Net.Security.SslStream.EnsureFullTlsFrameAsync[TIOAdapter](CancellationToken cancellationToken)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at System.Net.Security.SslStream.ReadAsyncInternal[TIOAdapter](Memory`1 buffer, CancellationToken cancellationToken)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at System.IO.Stream.<CopyToAsync>g__Core|27_0(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken)
at System.Net.Http.HttpConnection.RawConnectionStream.CompleteCopyToAsync(Task copyTask, HttpConnection connection, 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)
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 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)
Server-side exception:
Details
System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake.
at System.Net.WebSockets.ManagedWebSocket.ThrowIfEOFUnexpected(Boolean throwOnPrematureClosure)
at System.Net.WebSockets.ManagedWebSocket.EnsureBufferContainsAsync(Int32 minimumRequiredBytes, CancellationToken cancellationToken, Boolean throwOnPrematureClosure)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token)
at System.Net.WebSockets.ManagedWebSocket.ReceiveAsyncPrivate[TResult](Memory`1 payloadBuffer, CancellationToken cancellationToken)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
at Program.<>c.<<<Main>$>b__0_1>d.MoveNext() in C:\Users\brecon.REDMOND\source\repos\WebApplication6\WebApplication6\Program.cs:line 22
--- End of stack trace from previous location ---
Regression?
N/A
Known Workarounds
Don't use the new WebSocket ConnectAsync API.
Configuration
No response
Other information
No response
Reactions are currently unavailable
Activity
MihaZupan commentedon Jul 19, 2022
cc: @greenEkatherine
For reference, YARP has the same sort of issue and we deal with that by just throwing if you use
HttpClient:https://github.com/microsoft/reverse-proxy/blob/903b7cd13ce64e045899b286838a4818b6ab77bf/src/ReverseProxy/Forwarder/HttpForwarder.cs#L99-L102
We really want you to just use
new HttpMessageInvoker(whateverHandler)instead.BrennanConroy commentedon Jul 19, 2022
That would be poor behavior for this API IMO. You likely already have an HttpClient when using this API, otherwise you would just call the old
ConnectAsyncAPI.The fix is likely to test for
HttpClientand call theSendAsyncoverload that acceptsHttpCompletionOption.karelz commentedon Aug 1, 2022
Triage: Agreed that the new API will be likely used the way @BrennanConroy used it. We should accommodate for that -- and as he suggests, use the other overload for HttpClient-derived classes.