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

  1. Point the request at the final, canonical URL (https, correct host, trailing slash) so no redirect happens — this is the usual fix.
  2. Replace the generator/iterator body with a seekable object: read it into `io.BytesIO` or a temp file before sending.
  3. Set `allow_redirects=False`, inspect `resp.headers['Location']`, recreate the generator, and resend manually.
  4. 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

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


AI-assisted analysis of psf/requests@414f0513c3 (2026-07-31). Data as JSON: /data/errors/e35d8ce52f5ac8c3.json. Report an issue: GitHub.