RuntimeError "aclose(): asynchronous generator is already running" from nested stream generators at event loop shutdown
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 25/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Stale
- Tech stack
- python
- Domain
- backend, networking
Research direction
Start with the reported cleanup paths in httpx2/_models.py (Response.aiter_bytes and Response.aiter_raw), then trace safe_async_iterate and the stream implementations mentioned in the issue. Run the reproduction with several open streams during event loop shutdown and compare against httpx 0.28.1. Done means shutdown closes the streams without aclose(): asynchronous generator is already running; an open linked pull request indicates work is already underway.
Written by the indexing model from the issue text.
Description
Drafted by Claude (AI assistant) in discussion with @Seluj78, who reviewed and is filing it.
Summary
When an event loop shuts down while streamed responses are still open, loop.shutdown_asyncgens() closes every live async generator concurrently. httpx2's streaming generators are nested with contextlib.aclosing(...) / safe_async_iterate(...), so an outer generator's aclose() tries to close an inner generator whose own aclose() is already in flight, and raises:
an error occurred during closing of asynchronous generator <async_generator object Response.aiter_bytes at 0x...>
asyncgen: <async_generator object Response.aiter_bytes at 0x...>
Traceback (most recent call last):
File ".../httpx2/_models.py", line 996, in aiter_bytes
yield chunk
GeneratorExit
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../httpx2/_models.py", line 992, in aiter_bytes
async with contextlib.aclosing(self.aiter_raw()) as raw_stream:
File ".../contextlib.py", line 386, in __aexit__
await self.thing.aclose()
RuntimeError: aclose(): asynchronous generator is already running
The same error is reported for Response.aiter_raw, PoolByteStream.__aiter__ (async with safe_async_iterate(self._stream)) and HTTP11ConnectionByteStream.__aiter__.
httpx 0.28.1 with the same script reports nothing, so this looks like a regression from the added aclosing nesting. We hit it in production after upgrading openai to a release that depends on httpx2: every worker exit with in-flight streams logs a handful of these errors via asyncio's exception handler.
Reproduction
import asyncio
import httpx2
async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
while (await reader.readline()) not in (b"\r\n", b""):
pass
writer.write(b"HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n5\r\nhello\r\n")
await writer.drain()
await asyncio.sleep(3600) # keep the body open
streams = []
async def main() -> None:
server = await asyncio.start_server(handle, "127.0.0.1", 0)
url = f"http://127.0.0.1:{server.sockets[0].getsockname()[1]}/"
client = httpx2.AsyncClient()
for _ in range(5):
response = await client.send(client.build_request("GET", url), stream=True)
chunks = response.aiter_bytes()
await anext(chunks)
streams.append(chunks) # still open when asyncio.run() finalizes async generators
asyncio.run(main())
| Environment | Result |
|---|---|
| httpx2 2.14.0, CPython 3.11.13 | 5x RuntimeError: aclose(): asynchronous generator is already running |
| httpx2 2.14.0, CPython 3.13.7 | 5x same |
httpx 0.28.1 (same script, httpx2 → httpx), CPython 3.11.13 |
no errors |
With a single open stream there is usually no error; it needs several generators being finalized in the same gather().
Expected
Shutting down with open streams should close them quietly, as httpx 0.28 does.
Possible directions
- Make the nested
aclose()tolerant of the inner generator already being closed by someone else (e.g. skip/suppress when the inner generator is already running or finished duringGeneratorExithandling). - Avoid nesting generator ownership (have the outer generator iterate the inner stream without
aclosing, and rely onResponse.aclose()/ the stream's ownaclose()for cleanup).
- Dominant language
- Python
- Stars
- 1.6k
- Forks
- 103
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 25
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from pydantic/httpx2
-
Please revert aiter_bytes signature to AsyncIteratorMay be free again A pull request for this issue was closed without being merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
pydantic/httpx2#1158 · 2 comments ·
Maintainers usually reply within 1 day
-
Export AutoBackendMay be free again A pull request for this issue was closed without being merged. Open
Difficulty 1/5 Under an hour Newbie friendliness 92/100
pydantic/httpx2#1131 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Maintainers usually reply within 1 day
-
Mention header name error message for invalid header encodingsMay be free again A pull request for this issue was closed without being merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
pydantic/httpx2#864 · 3 comments ·
Maintainers usually reply within 1 day
-
Support DigestAuth with "x-www-authenticate" in first response headerPossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
Similar issues
-
Email validation accepts a trailing newlinePossibly taken @marcello-pichini claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
dbt-labs/spark-utils#51 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
bug frontend
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
vendrabuck/HERD#1419 ·
Maintainers usually reply within 1 day