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 httpx.StreamConsumed by IteratorByteStream.__iter__ (and the async variant) when a generator-backed body stream is iterated a second time. Generators cannot be replayed, so once consumed the stream is exhausted; httpx guards re-iteration for generator sources (file-like sources are exempt because they expose .read).

Source

Thrown at httpx/_content.py:52

    def __iter__(self) -> Iterator[bytes]:
        yield self._stream

    async def __aiter__(self) -> AsyncIterator[bytes]:
        yield self._stream


class IteratorByteStream(SyncByteStream):
    CHUNK_SIZE = 65_536

    def __init__(self, stream: Iterable[bytes]) -> None:
        self._stream = stream
        self._is_stream_consumed = False
        self._is_generator = inspect.isgenerator(stream)

    def __iter__(self) -> Iterator[bytes]:
        if self._is_stream_consumed and self._is_generator:
            raise StreamConsumed()

        self._is_stream_consumed = True
        if hasattr(self._stream, "read"):
            # File-like interfaces should use 'read' directly.
            chunk = self._stream.read(self.CHUNK_SIZE)
            while chunk:
                yield chunk
                chunk = self._stream.read(self.CHUNK_SIZE)
        else:
            # Otherwise iterate.
            for part in self._stream:
                yield part


class AsyncIteratorByteStream(AsyncByteStream):
    CHUNK_SIZE = 65_536

    def __init__(self, stream: AsyncIterable[bytes]) -> None:

View on GitHub (pinned to b5addb64f0)

Solutions

  1. Buffer the body as bytes (content=b"...") or a seekable file object instead of a raw generator.
  2. Wrap the generator so the body can be replayed, or read it fully before sending if redirects are expected.
  3. For responses, call response.read()/aread() once and reuse response.content.

Example fix

// before
def gen(): yield b"chunk"
client.post(url, content=gen(), follow_redirects=True)  # StreamConsumed on redirect
// after
client.post(url, content=b"chunk", follow_redirects=True)
Defensive patterns

Strategy: validation

Validate before calling

import inspect

def is_replayable(content) -> bool:
    # generators/async generators cannot be replayed once consumed
    if inspect.isgenerator(content) or inspect.isasyncgen(content):
        return False
    return True

# before sending with redirects or auth that may resend the body
assert is_replayable(content) or not follow_redirects, \
    "generator content cannot survive a redirect/auth resend; pass bytes or a file"

Type guard

import inspect

def is_replayable_content(content) -> bool:
    if inspect.isgenerator(content) or inspect.isasyncgen(content):
        return False
    return True

Try / catch

try:
    resp = client.post(url, content=body, follow_redirects=True)
except httpx.StreamConsumed:
    # body was a one-shot generator; retry with buffered bytes
    resp = client.post(url, content=buffered_bytes, follow_redirects=True)

Prevention

When it happens

Trigger: Passing a generator as request content= and then hitting a redirect or an auth retry that must resend the body (the generator is already exhausted); or iterating response content twice (calling response.read()/aiter() again on a generator-backed stream).

Common situations: Streaming uploads that get redirected; DigestAuth challenges requiring a body resend; calling .iter_bytes() / .read() on the same streamed response more than once.

Related errors


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