Skip to content

WebSockets over HTTP/1.1 with HttpClient fails #72476

Closed

Description

@BrennanConroy

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

Activity

  1. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Jul 19, 2022
  2. MihaZupan commented on Jul 19, 2022

    @MihaZupan
    Member

    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.

  3. BrennanConroy commented on Jul 19, 2022

    @BrennanConroy
    MemberAuthor

    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 ConnectAsync API.

    The fix is likely to test for HttpClient and call the SendAsync overload that accepts HttpCompletionOption.

  4. added and removed
    untriagedNew issue has not been triaged by the area owner
    on Jul 21, 2022
  5. added this to the 7.0.0 milestone on Jul 21, 2022
  6. karelz commented on Aug 1, 2022

    @karelz
    Member

    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.

  7. ghost added
    in-prThere is an active PR which will close this issue when it is merged
    on Aug 5, 2022
  8. ghost removed
    in-prThere is an active PR which will close this issue when it is merged
    on Aug 9, 2022
  9. ghost locked as resolved and limited conversation to collaborators on Sep 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions