encode/httpx · error · StreamConsumed
Attempted to read or stream some content, but the content…
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.
Solutions
- Buffer the body once with `response.read()` and reuse `response.content` for subsequent access.
- For request bodies that may be replayed (redirects/auth), pass bytes or a file object with `seek()`, not a generator.
- Tee the stream into an in-memory buffer if you genuinely need multiple passes.
- 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
- Buffer once with .read() and reuse .content for multiple consumers.
- Never pass a generator as request content if redirects/auth may replay it.
- Tee into memory only when truly necessary.
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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Attempted to access streaming response content, without…
- Attempted to read or stream content, but the stream has…
- Attempted to call a sync close on an async stream.
- Attempted to call a sync iterator on an async stream.
- Attempted to call an async close on a sync stream.
AI-assisted analysis of encode/httpx@b5addb64f0 (2026-08-04).
Data as JSON: /api/errors/266a188feb523d88.
Report an issue: GitHub.
Appendix: 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)