Skip to content

HttpClient: Deadlock in SendAsync when AllowDuplex is true under specific conditions #96223

@huntharo

Description

@huntharo

Description

HttpClient will not send the HEADERS frame for requests that do have Content but do not have EndStream set when SendHeadersAsync is called. It appears the only case that this applies to may be AllowDuplex = true in a custom Content object.

Reproduction Steps

Complete example code:

https://github.com/huntharo/httpclient-duplex-deadlock

Video demonstrating the issue and the fix:

Reproduction of the deadlock issue in HttpClient and demonstration of possible fixes

Expected behavior

It seems that SendHeadersAsync should always cause a flush as:

  • Causing a flush when headers are written appears to be happening for everything except AllowDuplex = true
  • Holding headers back seems like the wrong thing to do, ever, in that it will prevent the remote server from starting the request - delays here are bad
  • At the very least, requests that have AllowDuplex = true should also be added to the mustFlush list

Actual behavior

The behavior is changed by subsequent requests sent on the same connection:

  • Subsequent AllowDuplex = true requests can cause a flush if there are enough parallel requests sent AND they have enough headers set (the values may need to be unique to avoid deduplication)
    • Under the right conditions this can make it appear that the problem does not exist
  • Subsequent requests that set ExpectContinue will cause a flush
    • Setting ExpectContinue will always cause this to work, but results in an additional round trip (confirm?) and is not desirable
  • Requests that either have no Content or that have EndStream set (essentially: AllowDuplex = false) will cause a flush
    • Sending a GET request (or POST with ready-at-send Content) will cause the stuck request to send its HEADERS frame
    • The HEADERS frame will be delayed up to the time that the GET request is sent

Failure to use one of the above workarounds will cause the request to timeout without ever sending the HEADERS frame.

Regression?

  • Did not test prior versions of dotnet
  • There is issue HTTP2: Support bidirectional content #1511
    • This issue says to support bidirectional content over Http2
    • The support is nearly 100% there, this was the only issue I discovered in hundreds of millions of requests that read the response body (to completion) before sending the request body
    • My project was observing periodic 5 second hangs that corresponded to a 5 second interval of a "ping" GET request sent over the same HttpClient

Known Workarounds

See actual behavior, but it appears that ExpectContinue = true is a workaround, as is sending a ton of headers.

Configuration

  • dotnet 8.0 / also release/8.0 branch built locally as of 2023-12-20
  • Mac OS X Sonoma
  • ARM64
  • The problem does not appear to be specific to this configuration at all

Other information

Possibly the simplest fix is that the callback in SendHeadersAsync should always return true, causing a flush. The nominal case of non-duplex requests essentially always causes this to return true and it seems only this odd case of duplex requests is causing it to return false.

However, a more narrow fix is to add in mustFlush: ... || duplex here after moving the bool duplex = line to be above the SendHeadersAsync call:

Http2Stream http2Stream = await SendHeadersAsync(request, cancellationToken, mustFlush: shouldExpectContinue || request.IsExtendedConnectRequest).ConfigureAwait(false);

I would love to make the PR and add tests for this after the approach is approved.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions