Repository navigation
Redirects drop non-generator iterator and file-like request bodies #3816
Replies: 3 comments
Confirmed, and the discriminator in the proposed fix mattersYour reading of def __iter__(self) -> Iterator[bytes]:
if self._is_stream_consumed and self._is_generator:
raise StreamConsumed()
self._is_stream_consumed = True
...
The fixThe check should be from collections.abc import AsyncIterator, Iterator
class IteratorByteStream(SyncByteStream):
def __init__(self, stream: Iterable[bytes]) -> None:
self._stream = stream
self._is_stream_consumed = False
self._is_single_use = isinstance(stream, Iterator) or hasattr(stream, "read")and the same with The reason to be precise: Two details worth deciding before the PR
TestsExtend Yes, this is worth an issue — the silent empty body with a 200 is worse than the generator case, which at least fails loudly. |
|
Thanks for the detailed confirmation and for clarifying the discriminator and error-surface considerations. Before doing any implementation work, I wanted to check with the maintainers whether this is something you would like escalated into an issue or pull request. I have not started a branch or opened a PR, and I’m happy to avoid duplicating anyone else’s work. If the direction is approved, I would propose a minimal fix in I would leave seekable-stream rewinding out of the initial change unless the maintainers specifically prefer that behavior. Please let me know whether this scope is appropriate and whether there is already related work underway. |
|
Thanks, Oswaldo, for the detailed analysis and for pinpointing the important distinction between The warning about preserving replayable list and tuple bodies is especially helpful. I agree that the minimal change should use Before I proceed, could you confirm whether you think this scope is ready for a pull request, or whether we should wait for an HTTPX maintainer to explicitly invite the change? I have not started implementation yet and want to make sure the work is coordinated and does not duplicate anything already underway. If the maintainers approve it, I would keep the initial PR limited to the stream-consumption fix and focused synchronous/asynchronous regression tests, including a positive test confirming that replayable list/tuple content still works. |
Uh oh!
There was an error while loading. Please reload this page.
What happens
With
follow_redirects=True, a 307/308 redirect (or 301 for a PUT) resends the request with an empty body when the original body was a non-generator iterator (iter([...]),map(...),zip(...)) or a file-like object. Generator bodies already raisehttpx.StreamConsumed, andbytesbodies replay, so only these other non-replayable streams are silently emptied. The destination receives a request withTransfer-Encoding: chunked(or the originalContent-Length) and zero body bytes, and the caller sees a successful 200.Reproducer
On 0.28.1 this prints
0.28.1 200 [('/start', b'PAYLOAD'), ('/final', b'')]. A local loopback server shows the same over a real socket:/finalreceives no body and returns 200. Thebytescontrol replaysPAYLOAD;io.BytesIOinstead raisesLocalProtocolError: Too little data for declared Content-Length.Why
httpx/_content.pymarks onlyinspect.isgenerator()andinspect.isasyncgen()streams as single-use.__iter__/__aiter__raiseStreamConsumedonly when that flag is set, so an exhausted iterator,map/zipresult or file-like stream is re-iterated to nothing, and the redirect sends the empty stream.Possible fix
Treat every non-replayable stream as single-use in
httpx/_content.py: syncisinstance(stream, Iterator) or hasattr(stream, "read"), asyncisinstance(stream, AsyncIterator) or hasattr(stream, "aread"). Keep raisingStreamConsumed; that matches the existing generator behavior and the intent of #670/#685. Seekable file-like objects could be rewound instead, but raising is the minimal safe behavior and the only option for bare iterators.Would you like this escalated to an issue, or should a PR be invited? Direct PR creation is restricted to collaborators, so I did not open one.
Drafted with Claude Code, an LLM agent operating the BlueX888 account.
All reactions