Skip to content

Exceptions not handled properly in the proxy's AsyncTunnelHTTPConnection leading to the leaks and pool starvation #1023

Description

@janosik47

The problem is described in #921

I made a proposal how to fix it in a comment

Activity

  1. rail-chamidullin-myriad commented on Dec 12, 2025

    @rail-chamidullin-myriad

    @janosik47 I am dealing exactly with the same issue and I got to the same conclusion as you. Any update on this? Is there some PR with the fix?

  2. janwijbrand commented on Apr 9, 2026

    @janwijbrand

    FWIW to move forward on this issue, I opted for a monkey patch in my code base. Hopefully only temporary. The monkey patch looks more or less like this:

    _original_handle_async_request = AsyncTunnelHTTPConnection.handle_async_request
    
    @functools.wraps(_original_handle_async_request)
    async def _patched_handle_async_request(self, request):
        """Wrapper around ``handle_async_request`` that cleans up the inner
        connection when a tunnel setup error (e.g. TLS handshake failure)
        leaves it as a zombie.
        """
        try:
            return await _original_handle_async_request(self, request)
        except BaseException:
            if not self._connected and not self._connection.is_closed():
                with AsyncShieldCancellation():
                    await self._connection.aclose()
            raise
    
    def apply():
        if (
            AsyncTunnelHTTPConnection.handle_async_request
            is not _patched_handle_async_request
        ):
            AsyncTunnelHTTPConnection.handle_async_request = (
                _patched_handle_async_request
            )
    
  3. Ctrl-Creeper commented on Nov 4, 2026

    @Ctrl-Creeper

    I reproduced this against current main / httpcore 1.0.9 using real loopback sockets and have a focused cancellation regression plus patch here:

    Ctrl-Creeper@b28d0c4

    The cancellation path is important: after an HTTP proxy returns CONNECT 200, cancelling the caller during TLS negotiation leaves the inner HTTP/1.1 connection ACTIVE. It is neither idle nor expired, and the request has already left the pool queue. I observed this in a long-running messaging gateway: 228 CLOSE_WAIT sockets and descriptor exhaustion; runtime inspection mapped the accumulating sockets to general-pool AsyncTunnelHTTPConnection instances still in the CONNECT phase. Other requests could continue succeeding alongside those failed tunnels.

    The new regression runs a local CONNECT server that never completes TLS and verifies BOTH peer-observed EOF and an empty pool.connections before closing the pool. It covers AnyIO on asyncio, AnyIO on Trio, native Trio, external cancellation, and TLS EOF. On unpatched main: 6 failures / 2 backend skips. With the focused patch: the full suite passes (220 passed, 2 skipped; 6 existing xfail tests XPASS).

    Closing only the low-level stream fixes the descriptor leak but still leaves an ACTIVE pool entry, so that is insufficient. Closing the inner connection under AsyncShieldCancellation after start_tls fails releases both the TCP stream and pool slot, then re-raises the original error/cancellation. The production change is limited to that TLS setup failure path.

    This supplements #1049 / #1056 / #1071: an except Exception handler alone misses CancelledError (a BaseException), and cleanup needs to complete under the active cancellation scope. I have not opened another issue or competing PR; the ready-to-review patch/test commit is linked above.

    Additional validation using the affected gateway's installed dependencies: 300 cancelled CONNECT/TLS attempts with max_connections=1 ended with 0 CLOSE_WAIT, 0 retained pool entries, and the starting descriptor count. An unrelated in-flight request on the same client also completed normally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions