Skip to content

Commit 49a5855

Browse files
committed
Fix ConnectionPool state after async cancellation
1 parent a173552 commit 49a5855

22 files changed

+93
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

7+
## [Unreleased]
8+
9+
- Fix `ConnectionPool` state after async cancellation.
10+
11+
712
## Version 1.0.7 (November 15th, 2024)
813

914
- Support `proxy=…` configuration on `ConnectionPool()`. (#974)

httpcore/_async/connection.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ async def aclose(self) -> None:
172172
async with Trace("close", logger, None, {}):
173173
await self._connection.aclose()
174174

175+
def is_connected(self) -> bool:
176+
return not (self._connection is None or self._connection.is_closed())
177+
175178
def is_available(self) -> bool:
176179
if self._connection is None:
177180
# If HTTP/2 support is enabled, and the resulting connection could

httpcore/_async/connection_pool.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,13 +278,17 @@ def _assign_requests_to_connections(self) -> list[AsyncConnectionInterface]:
278278
those connections to be handled seperately.
279279
"""
280280
closing_connections = []
281+
request_connections = {request.connection for request in self._requests}
281282

282283
# First we handle cleaning up any connections that are closed,
283284
# have expired their keep-alive, or surplus idle connections.
284285
for connection in list(self._connections):
285286
if connection.is_closed():
286287
# log: "removing closed connection"
287288
self._connections.remove(connection)
289+
elif not (connection.is_connected() or connection in request_connections):
290+
# log: "removing garbage connection"
291+
self._connections.remove(connection)
288292
elif connection.has_expired():
289293
# log: "closing expired connection"
290294
self._connections.remove(connection)

httpcore/_async/http11.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ async def aclose(self) -> None:
264264
def can_handle_request(self, origin: Origin) -> bool:
265265
return origin == self._origin
266266

267+
def is_connected(self) -> bool:
268+
return not self.is_closed()
269+
267270
def is_available(self) -> bool:
268271
# Note that HTTP/1.1 connections in the "NEW" state are not treated as
269272
# being "available". The control flow which created the connection will

httpcore/_async/http2.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,9 @@ async def _wait_for_outgoing_flow(self, request: Request, stream_id: int) -> int
499499
def can_handle_request(self, origin: Origin) -> bool:
500500
return origin == self._origin
501501

502+
def is_connected(self) -> bool:
503+
return not self.is_closed()
504+
502505
def is_available(self) -> bool:
503506
return (
504507
self._state != HTTPConnectionState.CLOSED

httpcore/_async/http_proxy.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ async def aclose(self) -> None:
214214
def info(self) -> str:
215215
return self._connection.info()
216216

217+
def is_connected(self) -> bool:
218+
return self._connection.is_connected()
219+
217220
def is_available(self) -> bool:
218221
return self._connection.is_available()
219222

@@ -351,6 +354,9 @@ async def aclose(self) -> None:
351354
def info(self) -> str:
352355
return self._connection.info()
353356

357+
def is_connected(self) -> bool:
358+
return self._connection.is_connected()
359+
354360
def is_available(self) -> bool:
355361
return self._connection.is_available()
356362

httpcore/_async/interfaces.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ def info(self) -> str:
9494
def can_handle_request(self, origin: Origin) -> bool:
9595
raise NotImplementedError() # pragma: nocover
9696

97+
def is_connected(self) -> bool:
98+
"""
99+
Return `True` if the connection is open.
100+
101+
Beware that for some implementations `is_connected() != not is_closed()`.
102+
"""
103+
raise NotImplementedError() # pragma: nocover
104+
97105
def is_available(self) -> bool:
98106
"""
99107
Return `True` if the connection is currently able to accept an

httpcore/_async/socks_proxy.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ async def aclose(self) -> None:
305305
if self._connection is not None:
306306
await self._connection.aclose()
307307

308+
def is_connected(self) -> bool:
309+
return not (self._connection is None or self._connection.is_closed())
310+
308311
def is_available(self) -> bool:
309312
if self._connection is None: # pragma: nocover
310313
# If HTTP/2 support is enabled, and the resulting connection could

httpcore/_sync/connection.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ def close(self) -> None:
172172
with Trace("close", logger, None, {}):
173173
self._connection.close()
174174

175+
def is_connected(self) -> bool:
176+
return not (self._connection is None or self._connection.is_closed())
177+
175178
def is_available(self) -> bool:
176179
if self._connection is None:
177180
# If HTTP/2 support is enabled, and the resulting connection could

httpcore/_sync/connection_pool.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,13 +278,17 @@ def _assign_requests_to_connections(self) -> list[ConnectionInterface]:
278278
those connections to be handled seperately.
279279
"""
280280
closing_connections = []
281+
request_connections = {request.connection for request in self._requests}
281282

282283
# First we handle cleaning up any connections that are closed,
283284
# have expired their keep-alive, or surplus idle connections.
284285
for connection in list(self._connections):
285286
if connection.is_closed():
286287
# log: "removing closed connection"
287288
self._connections.remove(connection)
289+
elif not (connection.is_connected() or connection in request_connections):
290+
# log: "removing garbage connection"
291+
self._connections.remove(connection)
288292
elif connection.has_expired():
289293
# log: "closing expired connection"
290294
self._connections.remove(connection)

0 commit comments

Comments
 (0)