encode/httpx · error · RuntimeError

Attempted to call a sync close on an async stream.

Error message

Attempted to call a sync close on an async stream.

What it means

Raised as `RuntimeError` by sync `close()` when `self.stream` is not a `SyncByteStream`. You called the sync close on a Response whose underlying stream is async (came from `AsyncClient`). Closing an async stream synchronously would not release the connection correctly, so it is rejected.

Source

Thrown at httpx/_models.py:967

        with request_context(request=self._request):
            for raw_stream_bytes in self.stream:
                self._num_bytes_downloaded += len(raw_stream_bytes)
                for chunk in chunker.decode(raw_stream_bytes):
                    yield chunk

        for chunk in chunker.flush():
            yield chunk

        self.close()

    def close(self) -> None:
        """
        Close the response and release the connection.
        Automatically called if the response body is read to completion.
        """
        if not isinstance(self.stream, SyncByteStream):
            raise RuntimeError("Attempted to call a sync close on an async stream.")

        if not self.is_closed:
            self.is_closed = True
            with request_context(request=self._request):
                self.stream.close()

    async def aread(self) -> bytes:
        """
        Read and return the response content.
        """
        if not hasattr(self, "_content"):
            self._content = b"".join([part async for part in self.aiter_bytes()])
        return self._content

    async def aiter_bytes(
        self, chunk_size: int | None = None
    ) -> typing.AsyncIterator[bytes]:
        """

View on GitHub (pinned to b5addb64f0)

Solutions

  1. Use `await response.aclose()` for async responses.
  2. Inside an async context manager, prefer `async with client.stream(...)` which acloses automatically.
  3. Branch on transport type if a shared cleanup helper is unavoidable.
  4. Keep sync/async response lifecycles fully separated.

Example fix

// before
r = await async_client.get(url)
r.close()  # RuntimeError

// after
r = await async_client.get(url)
await r.aclose()
Defensive patterns

Strategy: type-guard

Validate before calling

from httpx._transports.default import AsyncByteStream

def needs_aclose(resp: httpx.Response) -> bool:
    return isinstance(resp.stream, AsyncByteStream)

Type guard

import httpx
from httpx._transports.default import AsyncByteStream

def is_async_stream(resp: httpx.Response) -> bool:
    return isinstance(resp.stream, AsyncByteStream)

Try / catch

try:
    response.close()
except RuntimeError:
    await response.aclose()

Prevention

When it happens

Trigger: Calling `response.close()` (sync) on a Response produced by `httpx.AsyncClient`.

Common situations: Same as error 48: mixing sync/async clients, partial async migration, shared helpers, or framework finalizers that call `.close()` regardless of the transport type.

Related errors


AI-assisted analysis of encode/httpx@b5addb64f0 (2026-08-04). Data as JSON: /data/errors/7154502aebe06f73.json. Report an issue: GitHub.