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 ↗.