encode/httpx · error · StreamClosed

Attempted to read or stream content, but the stream has been

Error message

Attempted to read or stream content, but the stream has been closed.

What it means

Raised as `StreamClosed` by sync `iter_raw()` when `is_closed` is True. Once a response stream is closed (explicitly via `.close()` or implicitly by being read to completion), further reads are rejected because the connection has been released back to the pool.

Source

Thrown at httpx/_models.py:942

                yield chunk

    def iter_lines(self) -> typing.Iterator[str]:
        decoder = LineDecoder()
        with request_context(request=self._request):
            for text in self.iter_text():
                for line in decoder.decode(text):
                    yield line
            for line in decoder.flush():
                yield line

    def iter_raw(self, chunk_size: int | None = None) -> typing.Iterator[bytes]:
        """
        A byte-iterator over the raw response content.
        """
        if self.is_stream_consumed:
            raise StreamConsumed()
        if self.is_closed:
            raise StreamClosed()
        if not isinstance(self.stream, SyncByteStream):
            raise RuntimeError("Attempted to call a sync iterator on an async stream.")

        self.is_stream_consumed = True
        self._num_bytes_downloaded = 0
        chunker = ByteChunker(chunk_size=chunk_size)

        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()

View on GitHub (pinned to b5addb64f0)

Solutions

  1. Perform all reads inside the `with client.stream(...) as r:` block.
  2. Call `response.read()` once inside the context and then use `response.content` outside it.
  3. If you need the body later, switch to a non-streaming `client.get()` which reads and keeps the body.
  4. Track a `closed` flag in your own code to avoid touching a closed response.

Example fix

// before
with client.stream('GET', url) as r:
    pass
for c in r.iter_bytes():  # StreamClosed
    ...

// after
with client.stream('GET', url) as r:
    r.read()
print(r.text)  # buffered, safe outside the block
Defensive patterns

Strategy: validation

Validate before calling

def is_open(resp: httpx.Response) -> bool:
    return not resp.is_closed

Type guard

import httpx

def stream_is_open(resp: httpx.Response) -> bool:
    return not resp.is_closed

Try / catch

try:
    for chunk in response.iter_bytes():
        ...
except httpx.StreamClosed:
    # re-issue the request for a fresh stream
    response = client.get(response.request.url, stream=True)

Prevention

When it happens

Trigger: Calling `response.iter_raw()` / `iter_bytes()` after `response.close()`, or after the `with client.stream(...)` context exited (which closes the response).

Common situations: Stepping outside a `with client.stream(...) as r:` block and then trying to read the body; calling `.read()` inside the block, exiting, then iterating; double close from a finally block.

Related errors


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