encode/httpx · error · StreamConsumed

Attempted to read or stream some content, but the content ha

Error message

Attempted to read or stream some content, but the content has already been streamed. For requests, this could be due to passing a generator as request content, and then receiving a redirect response or a secondary request as part of an authentication flow.For responses, this could be due to attempting to stream the response content more than once.

What it means

Raised as `StreamConsumed` by the sync `iter_raw()` method when `is_stream_consumed` is already True. A streaming body can be read exactly once; a second attempt is rejected because the underlying network bytes are gone. The message notes both request-side (generator content + redirect/auth retry) and response-side (double-streaming) causes.

Source

Thrown at httpx/_models.py:940

                yield chunk  # pragma: no cover
            for chunk in chunker.flush():
                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

View on GitHub (pinned to b5addb64f0)

Solutions

  1. Buffer the body once with `response.read()` and reuse `response.content` for subsequent access.
  2. For request bodies that may be replayed (redirects/auth), pass bytes or a file object with `seek()`, not a generator.
  3. Tee the stream into an in-memory buffer if you genuinely need multiple passes.
  4. Re-issue the request if you need a fresh stream.

Example fix

// before
for chunk in response.iter_bytes():
    ...
for chunk in response.iter_bytes():  # StreamConsumed
    ...

// after
response.read()
body = response.content  # reuse as often as needed
Defensive patterns

Strategy: validation

Validate before calling

def can_iter(resp: httpx.Response) -> bool:
    return not resp.is_stream_consumed and not resp.is_closed

Type guard

import httpx

def stream_is_fresh(resp: httpx.Response) -> bool:
    return not (resp.is_stream_consumed or resp.is_closed)

Try / catch

try:
    for chunk in response.iter_bytes():
        ...
except httpx.StreamConsumed:
    response.read()  # fall back to buffered content if already consumed once
    body = response.content

Prevention

When it happens

Trigger: Iterating `response.iter_raw()` / `iter_bytes()` a second time; or on the request side, passing a generator as `content=` and letting httpx replay it after a redirect or during an auth challenge (the generator was already exhausted).

Common situations: Caching/memoizing a streaming response and then trying to iterate again; logging the body via `iter_bytes()` and then handing the same response to JSON parsing; using `yield`-based request bodies with redirects.

Related errors


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