aio-libs/aiohttp · error · ClientPayloadError

Cannot follow redirect with a consumed request body. Use…

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 during redirect handling (status 307/308, or 301/302 with non-POST methods) when the original request body has already been consumed and cannot be replayed. The body must be re-sent verbatim on these redirects; aiohttp refuses to silently send an empty body.

Solutions

  1. Send bytes instead of a stream so the body can be replayed.
  2. Use a seekable file-like object and ensure aiohttp can rewind it (open(path, 'rb')).
  3. Set allow_redirects=False, inspect the 3xx response, and re-issue the request yourself with a fresh body.
  4. Buffer the stream into bytes up front when the body is small enough.

Example fix

// before
async def gen(): ...
await session.post(url, data=gen())  # 307 redirect -> raises
// after
body = b'...'
await session.post(url, data=body)
# or:
await session.post(url, data=open('file.bin','rb'), allow_redirects=True)
Defensive patterns

Strategy: validation

Validate before calling

async def post_rewindable(session, url, body):
    if isinstance(body, (bytes, bytearray)):
        return await session.post(url, data=body)
    if hasattr(body, 'seek'):
        return await session.post(url, data=body)
    # buffer streaming body
    data = b''.join([c async for c in body]) if hasattr(body, '__aiter__') else bytes(body)
    return await session.post(url, data=data)

Type guard

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

Try / catch

from aiohttp import ClientPayloadError

try:
    resp = await session.post(url, data=body)
except ClientPayloadError as e:
    if 'consumed request body' in str(e):
        resp = await session.post(url, data=body_bytes, allow_redirects=False)
    else:
        raise

Prevention

When it happens

Trigger: POST/PUT of a non-rewindable stream (e.g. a streaming generator, a pipe, or a Payload backed by a consumed file) that receives a 307/308 redirect. Reading req._body.consumed returns True at redirect time.

Common situations: Uploading a large file from a stream that has been read once. Using async generators as request bodies against endpoints that redirect. Proxy/CGI backends that 307-redirect uploads.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/be3f472c6f872fb5. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/client.py:798

                        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 d041d4d0fd)