Stop nesting async generators in response streams

KL
21d ago
+75-37

Closes #1280.

When the event loop shuts down with streamed responses still open, loop.shutdown_asyncgens() closes every async generator concurrently. If one generator closes another inside aclosing, and the inner one awaits during cleanup (closing the response or connection), asyncio and the outer generator both call aclose() on it, and one of them gets RuntimeError: aclose(): asynchronous generator is already running. This has been there since #137.

This removes the nesting instead of the cleanup:

  • PoolByteStream.__aiter__ returns the inner stream iterator directly. It was a pure pass-through generator.
  • Response.aiter_raw() and Response.aiter_bytes() return one shared generator (identity decoder vs content decoder) instead of aiter_bytes() wrapping aiter_raw().

Connections are still closed at shutdown. A narrower fix that skipped cleanup on GeneratorExit silenced the errors but leaked sockets, so I dropped it.

aiter_raw and aiter_bytes are now plain methods returning an async generator, so inspect.isasyncgenfunction() returns False for them. Calling and iterating them works the same.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.