Problem
If response cleanup is interrupted, both PoolByteStream.aclose() and HTTP11ConnectionByteStream.aclose() mark themselves closed before their awaited cleanup finishes. A later aclose() therefore returns immediately instead of completing the connection-pool bookkeeping.
This is reachable when an asyncio task is cancelled again while unwinding an earlier cancellation. The result is a closed HTTP/1.1 connection that remains counted as active in the pool, eventually exhausting max_connections.
The behavior was reported with a full HTTPX reproducer in encode/httpx#3782.
Cause
Both async byte-stream wrappers assign _closed = True before awaiting the inner close operation. If cancellation escapes that await, the state says cleanup completed even though the pool request was never removed.
Expected behavior
An interrupted close should remain retryable. A subsequent aclose() should finish closing the HTTP/1.1 stream and remove the request and connection from the pool. Concurrent close calls should remain serialized.
Problem
If response cleanup is interrupted, both
PoolByteStream.aclose()andHTTP11ConnectionByteStream.aclose()mark themselves closed before their awaited cleanup finishes. A lateraclose()therefore returns immediately instead of completing the connection-pool bookkeeping.This is reachable when an asyncio task is cancelled again while unwinding an earlier cancellation. The result is a closed HTTP/1.1 connection that remains counted as active in the pool, eventually exhausting
max_connections.The behavior was reported with a full HTTPX reproducer in encode/httpx#3782.
Cause
Both async byte-stream wrappers assign
_closed = Truebefore awaiting the inner close operation. If cancellation escapes that await, the state says cleanup completed even though the pool request was never removed.Expected behavior
An interrupted close should remain retryable. A subsequent
aclose()should finish closing the HTTP/1.1 stream and remove the request and connection from the pool. Concurrent close calls should remain serialized.