Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 24 additions & 16 deletions httpx/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1267,10 +1267,12 @@ def close(self) -> None:
if self._state != ClientState.CLOSED:
self._state = ClientState.CLOSED

self._transport.close()
for transport in self._mounts.values():
if transport is not None:
transport.close()
try:
self._transport.close()
finally:
for transport in self._mounts.values():
if transport is not None:
transport.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup still stops at the first mounted transport that raises, so later mounts remain open. On ff7190e, I used the public Client(transport=..., mounts=...) API with a main transport whose close() raises, a first mounted transport whose close() also raises, and a second mounted transport that records cleanup. client.close() produced calls=['main', 'first']; the second mount was never closed, and the first mount's exception replaced the original main-transport exception. The async loops have the same short-circuit behavior.

To meet this PR's all-transports cleanup goal, each mounted cleanup needs to be attempted independently while preserving or aggregating the raised exceptions.

Disclosure: I ran the changed client module through Bandit → SARIF → Lumi Trace at NOQT (which ranked _client.py first), then verified this with the focused runtime repro above.


def __enter__(self: T) -> T:
if self._state != ClientState.UNOPENED:
Expand Down Expand Up @@ -1298,10 +1300,12 @@ def __exit__(
) -> None:
self._state = ClientState.CLOSED

self._transport.__exit__(exc_type, exc_value, traceback)
for transport in self._mounts.values():
if transport is not None:
transport.__exit__(exc_type, exc_value, traceback)
try:
self._transport.__exit__(exc_type, exc_value, traceback)
finally:
for transport in self._mounts.values():
if transport is not None:
transport.__exit__(exc_type, exc_value, traceback)


class AsyncClient(BaseClient):
Expand Down Expand Up @@ -1982,10 +1986,12 @@ async def aclose(self) -> None:
if self._state != ClientState.CLOSED:
self._state = ClientState.CLOSED

await self._transport.aclose()
for proxy in self._mounts.values():
if proxy is not None:
await proxy.aclose()
try:
await self._transport.aclose()
finally:
for proxy in self._mounts.values():
if proxy is not None:
await proxy.aclose()

async def __aenter__(self: U) -> U:
if self._state != ClientState.UNOPENED:
Expand Down Expand Up @@ -2013,7 +2019,9 @@ async def __aexit__(
) -> None:
self._state = ClientState.CLOSED

await self._transport.__aexit__(exc_type, exc_value, traceback)
for proxy in self._mounts.values():
if proxy is not None:
await proxy.__aexit__(exc_type, exc_value, traceback)
try:
await self._transport.__aexit__(exc_type, exc_value, traceback)
finally:
for proxy in self._mounts.values():
if proxy is not None:
await proxy.__aexit__(exc_type, exc_value, traceback)