psf/requests · error · UnrewindableBodyError
Unable to rewind request body for redirect.
Error message
Unable to rewind request body for redirect.
What it means
Raised as `requests.exceptions.UnrewindableBodyError` from `rewind_body()` in `src/requests/utils.py:1155`. This is the branch where the request body either has no `seek()` method at all or requests never recorded a valid integer `_body_position` (it records `IncompleteRead`/failure sentinels when the initial `tell()` failed). Without both, the already-consumed streamed body cannot be replayed on a redirect, so requests refuses to follow it with a corrupt/empty body.
Source
Thrown at src/requests/utils.py:1155
def rewind_body(prepared_request: PreparedRequest) -> None:
"""Move file pointer back to its recorded starting position
so it can be read again on redirect.
"""
body_seek = getattr(prepared_request.body, "seek", None)
if body_seek is not None and isinstance(
prepared_request._body_position,
integer_types,
):
try:
body_seek(prepared_request._body_position)
except OSError:
raise UnrewindableBodyError(
"An error occurred when rewinding request body for redirect."
)
else:
raise UnrewindableBodyError("Unable to rewind request body for redirect.")
View on GitHub ↗ (pinned to 414f0513c3)
Solutions
- Point the request at the final, canonical URL (https, correct host, trailing slash) so no redirect happens — this is the usual fix.
- Replace the generator/iterator body with a seekable object: read it into `io.BytesIO` or a temp file before sending.
- Set `allow_redirects=False`, inspect `resp.headers['Location']`, recreate the generator, and resend manually.
- For 307/308-heavy APIs, check whether the API client library offers a native retry/redirect mechanism that re-creates the body.
Example fix
# before
def gen():
for chunk in produce_chunks():
yield chunk
requests.post('http://api.example.com/upload', data=gen())
# after
import io
buf = io.BytesIO(b''.join(produce_chunks()))
requests.post('https://api.example.com/upload', data=buf) Defensive patterns
Strategy: try-catch
Validate before calling
def body_is_rewindable(body):
return isinstance(body, (bytes, str)) or (hasattr(body, 'seek') and hasattr(body, 'tell')) Try / catch
try:
resp = requests.post(url, data=body_stream)
except requests.exceptions.UnrewindableBodyError:
# body has no usable seek/tell — cannot replay for the redirect
resp = requests.post(url, data=body_stream_factory(), allow_redirects=False) Prevention
- Check hasattr(body, 'seek') before sending stream bodies to endpoints that may redirect
- Use a body factory (re-create the stream per attempt) instead of a one-shot generator
- Disable redirects for streaming uploads and handle 3xx explicitly
When it happens
Trigger: Redirected POST/PUT where the body is a generator (`data=my_generator()`), an iterator for chunked uploads, or any object without `seek`/`tell`; or a file-like object whose `tell()` raised during `prepare_body`, leaving `_body_position` as a non-integer sentinel.
Common situations: Chunked uploads using generators to APIs that issue http→https or trailing-slash redirects; load-balancer or auth-gateway redirects in front of upload endpoints; wrapping compression/encryption generators around file uploads.
Related errors
- An error occurred when rewinding request body for redirect.
- Streamed bodies and files are mutually exclusive.
- chunk_size must be an int, it is instead a {type(chunk_size)
- The content for this response was already consumed
- Exceeded {self.max_redirects} redirects.
AI-assisted analysis of psf/requests@414f0513c3 (2026-07-31).
Data as JSON: /data/errors/e35d8ce52f5ac8c3.json.
Report an issue: GitHub ↗.