aio-libs/aiohttp · error · RuntimeError

Cannot clone request after reading its content

Error message

Cannot clone request after reading its content

What it means

BaseRequest.clone() refuses to run after the body has been read (self._read_bytes is truthy). A clone copies the request metadata, but a half-consumed body stream cannot be coherently duplicated, so aiohttp guards the operation up front.

Solutions

  1. Call request.clone() BEFORE any body-reading call.
  2. In middleware, read from the clone, not the original request.
  3. Check request.can_read_body / request._read_bytes before deciding to read.

Example fix

# before
body = await request.read()
clone = request.clone(method='PUT')
# after
clone = request.clone(method='PUT')
body = await clone.read()
Defensive patterns

Strategy: validation

Validate before calling

if request._read_bytes:
    raise RuntimeError('already read')
clone = request.clone()

Type guard

def can_clone(req: BaseRequest) -> bool:
    return not req._read_bytes

Prevention

When it happens

Trigger: Awaiting request.read() / request.text() / request.json() / request.post() and then calling request.clone().

Common situations: Middleware that reads the body for logging or signature verification, then forwards a cloned request; body inspection followed by a downstream clone.

Related errors


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

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