aio-libs/aiohttp · error · RuntimeError

Cannot clone request after reading its content

Error message

Cannot clone request after reading its content

What it means

Raised as RuntimeError by BaseRequest.clone (aiohttp/web_request.py:209) when self._read_bytes is already truthy - i.e. the request body has been read via .read() / .text() / .json() / .post(). clone() shares the same underlying payload (self._payload) by reference, so once bytes have been consumed the clone would receive an empty or inconsistent body. aiohttp guards against this silently corrupting downstream consumers.

Source

Thrown at aiohttp/web_request.py:209

    def clone(
        self,
        *,
        method: str | _SENTINEL = sentinel,
        rel_url: StrOrURL | _SENTINEL = sentinel,
        headers: LooseHeaders | _SENTINEL = sentinel,
        scheme: str | _SENTINEL = sentinel,
        host: str | _SENTINEL = sentinel,
        remote: str | _SENTINEL = sentinel,
        client_max_size: int | _SENTINEL = sentinel,
    ) -> "BaseRequest":
        """Clone itself with replacement some attributes.

        Creates and returns a new instance of Request object. If no parameters
        are given, an exact copy is returned. If a parameter is not passed, it
        will reuse the one from the current request object.
        """
        if self._read_bytes:
            raise RuntimeError("Cannot clone request after reading its content")

        dct: dict[str, Any] = {}
        if method is not sentinel:
            dct["method"] = method
        if rel_url is not sentinel:
            new_url: URL = URL(rel_url)
            dct["url"] = new_url
            dct["path"] = str(new_url)
        if headers is not sentinel:
            # a copy semantic
            new_headers = HeadersDictProxy(CIMultiDict(headers))
            dct["headers"] = new_headers
            dct["raw_headers"] = tuple(
                (k.encode("utf-8"), v.encode("utf-8"))
                for k, v in new_headers._md.items()
            )

        message = self._message._replace(**dct)

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Call clone() before reading the body, then read from the clone (or pass the cached bytes through request['body'] in the middleware).
  2. Cache the read body in request['cached_body'] and have the downstream handler use it instead of re-reading.
  3. Restructure middleware to only inspect headers / transport-level info, leaving body reads to the final handler.

Example fix

// before
async def mw(handler, request):
    body = await request.read()      # populates _read_bytes
    log_body(body)
    new_req = request.clone(method="POST")  # RuntimeError
    return await handler(new_req)

# after
async def mw(handler, request):
    new_req = request.clone(method="POST")  # clone first
    body = await new_req.read()
    log_body(body)
    return await handler(new_req)
Defensive patterns

Strategy: validation

Validate before calling

if request._read_bytes is None:
    cloned = request.clone(method="POST")
else:
    raise RuntimeError("clone before reading body")

Type guard

def can_clone(request) -> bool:
    return request._read_bytes is None

Try / catch

try:
    new_req = request.clone(method="POST")
except RuntimeError:
    new_req = request  # or rebuild from cached body in request['cached_body']

Prevention

When it happens

Trigger: Calling request.clone(...)' after request.read() (or .text()/.json()/.post()) has populated _read_bytes. Common in middleware that reads the body for logging/validation and then attempts to clone the request to pass a modified copy downstream.

Common situations: Custom middleware that needs to rewrite headers (method, path) after the body has been inspected; auth handlers that decode JSON to inspect claims and then forward the request; debugging code that logs request.text() before the main handler.

Related errors


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