aio-libs/aiohttp · error · ClientPayloadError

Cannot follow redirect with a consumed request body. Use byt

Error message

Cannot follow redirect with a consumed request body. Use bytes, a seekable file-like object, or set allow_redirects=False.

What it means

Raised in the redirect branch of `_request` (client.py:788-794) as a `ClientPayloadError`. For 307/308 redirects (and 301/302 on non-POST methods) the request body must be replayed verbatim, but if the body payload has already been consumed (streamed out) it cannot be rewound. Rather than silently sending an empty body and corrupting the request, aiohttp fails fast. The fix is to give it a replayable body.

Source

Thrown at aiohttp/client.py:790

                        if (resp.status == 303 and resp.method != hdrs.METH_HEAD) or (
                            resp.status in (301, 302) and resp.method == hdrs.METH_POST
                        ):
                            method = hdrs.METH_GET
                            data = None
                            if headers.get(hdrs.CONTENT_LENGTH):
                                headers.pop(hdrs.CONTENT_LENGTH)
                        else:
                            # For 307/308, always preserve the request body
                            # For 301/302 with non-POST methods, preserve the request body
                            # https://www.rfc-editor.org/rfc/rfc9110#section-15.4.3-3.1
                            # Use the existing payload to avoid recreating it from
                            # a potentially consumed file.
                            #
                            # If the payload is already consumed and cannot be replayed,
                            # fail fast instead of silently sending an empty body.
                            if req._body.consumed:
                                resp.close()
                                raise ClientPayloadError(
                                    "Cannot follow redirect with a consumed request "
                                    "body. Use bytes, a seekable file-like object, "
                                    "or set allow_redirects=False."
                                )
                            data = req._body

                        r_url = resp.headers.get(hdrs.LOCATION) or resp.headers.get(
                            hdrs.URI
                        )
                        if r_url is None:
                            # see github.com/aio-libs/aiohttp/issues/2022
                            break
                        else:
                            # reading from correct redirection
                            # response is forbidden
                            resp.release()

                        try:

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Pass the body as `bytes` / `bytearray` so it can be replayed.
  2. Use a seekable file-like object and rewind before the redirect (`f.seek(0)`); aiohttp will seek on replay.
  3. Disable redirect following for that call: `allow_redirects=False`, then inspect the Location and re-issue manually.
  4. For one-shot streams, read the whole body into memory first (`data=await stream.read()`).

Example fix

// before
await session.post(url, data=some_pipe_stream)  # server returns 307
// after
body = b'...'  # or await f.read() into bytes
await session.post(url, data=body)
// or
await session.post(url, data=seekable_file, allow_redirects=False)
Defensive patterns

Strategy: validation

Validate before calling

def replayable_body(body) -> bool:
    return isinstance(body, (bytes, bytearray, str)) or (
        hasattr(body, 'read') and hasattr(body, 'seek')
    )

Type guard

def is_replayable_body(body) -> bool:
    return isinstance(body, (bytes, bytearray)) or (
        hasattr(body, 'seek') and hasattr(body, 'read')
    )

Prevention

When it happens

Trigger: POSTing/PUTing a non-seekable stream or an async generator as `data=` to a URL that returns 307/308 (which by spec must replay the body). Also when 301/302 is returned for a non-POST method (PUT/DELETE) — those preserve the body too.

Common situations: Uploading a file via a pipe or network stream that can't seek; passing an `aiohttp.streamer` async generator; servers that redirect POSTs with 307 instead of the more common 302 (which converts to GET and drops the body).

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/be3f472c6f872fb5.json. Report an issue: GitHub.