fix: clean up garbage connections on cancellation in AsyncConnectionPool
Merged
MB
3mo ago+110+110+012345678901234567890123456789-0-0-0123456789
Summary
- Fixes the
AsyncConnectionPoolpoisoning bug reported in #982: anasyncio.CancelledError(or any cancel scope timeout) during connection establishment leaves a NEW-state connection inself._connectionsthat is neither closed nor connected, eventually exhaustingmax_connectionsand surfacing asPoolTimeouton every subsequent request. - Adds
is_connected()to(Async)ConnectionInterface, overridden on the concrete connection classes and proxy wrappers so a connection in the pre-TCP NEW state reportsFalse. - Teaches
_assign_requests_to_connections()to recognise such garbage connections (not connected AND no live pool request references them) and silently drop them from the pool.
Ported from internal commit https://codeberg.org/httpxyz/httpcorexyz/commit/a7500e4 on the httpcorexyz fork
Reproduction (from #982)
import asyncio
import httpcore2 as httpcore
async def main():
async with httpcore.AsyncConnectionPool(max_connections=1) as pool:
tasks = [asyncio.create_task(pool.request("GET", f"http://localhost:8888/{i}")) for i in range(2)]
await asyncio.sleep(0)
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
print(pool)
# Before: <AsyncConnectionPool [Requests: 0 active, 0 queued | Connections: 1 active, 0 idle]>
# After: <AsyncConnectionPool [Requests: 0 active, 0 queued | Connections: 0 active, 0 idle]>
await pool.request("GET", "http://localhost:8888/", extensions={"timeout": {"pool": 1}})
# Before: httpcore2.PoolTimeout
# After: proceeds normally (or surfaces a genuine ConnectError/ConnectTimeout)
Test plan
-
scripts/checkโ ruff format/check + mypy clean -
scripts/testโ full suite passes (1651 passed, 1 skipped) on both asyncio and trio backends -
scripts/coverageโ 100% coverage maintained - New
test_connection_pool_cancellation_during_waiting_for_connectionverifies the pool is empty after cancellation in the connection-wait phase, on both asyncio and trio - Manually re-ran the reproduction from #982 against the patched pool โ pool stays empty and the follow-up request no longer hangs on a
PoolTimeout
Closes #982.
๐ค Generated with Claude Code