encode/httpx · error · StreamClosed

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

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.

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: /api/errors/cfb4f23ef2b47af7. Report an issue: GitHub.

Appendix: 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)