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
- Pass the body as `bytes` / `bytearray` so it can be replayed.
- Use a seekable file-like object and rewind before the redirect (`f.seek(0)`); aiohttp will seek on replay.
- Disable redirect following for that call: `allow_redirects=False`, then inspect the Location and re-issue manually.
- 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
- For methods that may 307/308 (POST/PUT/DELETE), pass bytes or seekable files.
- Set allow_redirects=False for one-shot streams and re-issue manually.
- Read streams fully into memory before posting if the body is small.
- Watch for `consumed` state on async-generator payloads.
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
- data and json parameters can not be used at the same time
- Server attempted redirecting to a location that does not loo
- Invalid redirect URL origin
- Invalid window size
- Extension for deflate not supported{ext}
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/be3f472c6f872fb5.json.
Report an issue: GitHub.