Repository navigation
[bug] TCP socket is left open when the TLS handshake is cancelled from outside (AnyIO backend start_tls) #1244
Unanswered
MrEricKing
asked this question in
Q&A
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Summary
If the task that is opening an HTTPS connection gets cancelled while the TLS handshake is in progress (for example by
asyncio.wait_for,anyio.fail_after, ortask.cancel()aroundclient.get(...)), the underlying TCP socket is never closed. If httpx2's ownconnecttimeout ends the handshake instead, the socket is closed correctly.Where
httpcore2/_backends/anyio.py,AnyIOStream.start_tls:Cancellation (
asyncio.CancelledError, or AnyIO's cancellation exception) is aBaseException, so it skips theexcept Exceptionblock andaclose()never runs. The caller cannot clean up either:AsyncHTTPConnection._connectkeeps the plain TCP stream in a local variable and has notry/finallyaroundstart_tls. #983 removes the half-open connection object from the pool after a cancellation, but the socket thatstart_tlswas wrapping stays open.Reproduction
httpx2 2.13.1, httpcore2 2.13.1, anyio 4.14.2, CPython 3.11.15, asyncio, macOS. The server accepts TCP and never answers the ClientHello.
Output (the same on repeated runs):
Suggested fix
Close the stream on any exit from the handshake, shielded so that the close itself is not cancelled:
With
start_tlspatched this way, the reproduction printsclient socket closedfor both cases. I did not check the Trio backend or the sync backend, and I have not run your test suite against this change.Related, but none of them covers this case: #983 (pool cleanup after cancellation during connect), #1243 (cancellation during HTTP/1.1 response cleanup), #1010 and #1226 (proxy tunnel when TLS fails with an exception). #1170's new asyncio backend also closes the transport only on timeout,
OSErrorandssl.SSLErrorinstart_tls, so cancellation would still skip the cleanup there.Workaround
We set the
connecttimeout shorter than our outer deadline, so httpx2's own timeout always ends a stuck handshake before the outer cancellation does.All reactions