You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Exceptions not handled properly in the proxy's AsyncTunnelHTTPConnection leading to the leaks and pool starvation #1023
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
)
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.
Activity
rail-chamidullin-myriad commentedon Dec 12, 2025
@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?
janwijbrand commentedon Apr 9, 2026
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:
Ctrl-Creeper commentedon Nov 4, 2026
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.connectionsbefore 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_tlsfails 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 Exceptionhandler 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.