aio-libs/aiohttp · error · TypeError

Unable to decode - content not cached. Call as_bytes()…

Error message

Unable to decode - content not cached. Call as_bytes() first.

What it means

Raised by AsyncIterablePayload.decode when there are no cached chunks to decode. Because an async iterable is consumed lazily and can only be read once, decode() cannot materialize the content; the chunks must first be populated by awaiting as_bytes(), which caches them. Calling decode() before that has nothing to work with.

Solutions

  1. Await as_bytes() first to populate the cache, then call decode(): data = await payload.as_bytes(); text = payload.decode().
  2. If you only need the string once, use text = (await payload.as_bytes()).decode('utf-8') directly.
  3. Be aware that caching loads the entire body into memory; for large streams prefer incremental processing.

Example fix

# before
text = payload.decode()

# after
await payload.as_bytes()  # populates _cached_chunks
text = payload.decode()
Defensive patterns

Strategy: validation

Validate before calling

if payload._cached_chunks is None:
    await payload.as_bytes()
text = payload.decode()

Type guard

def payload_is_cached(payload) -> bool:
    return getattr(payload, '_cached_chunks', None) is not None

Try / catch

try:
    text = payload.decode()
except TypeError:
    await payload.as_bytes()
    text = payload.decode()

Prevention

When it happens

Trigger: Calling payload.decode() on an AsyncIterablePayload that has never been awaited via as_bytes(); calling decode() after the iterable was consumed directly (e.g. async for chunk in payload) without going through as_bytes().

Common situations: Inspecting/debugging a streaming payload's body; reusing a payload after partial consumption; code that worked for BytesPayload.decode() reused on a streaming payload.

Understand the failure class

Related errors


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

Appendix: source

Thrown at aiohttp/payload.py:1078

                if remaining_bytes is None:
                    await writer.write(chunk)
                # If we have a content length limit
                elif remaining_bytes > 0:
                    await writer.write(chunk[:remaining_bytes])
                    remaining_bytes -= len(chunk)
                # We still want to exhaust the iterator even
                # if we have reached the content length limit
                # since the file handle may not get closed by
                # the iterator if we don't do this
        except StopAsyncIteration:
            # Iterator is exhausted
            self._iter = None

    def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str:
        """Decode the payload content as a string if cached chunks are available."""
        if self._cached_chunks is not None:
            return b"".join(self._cached_chunks).decode(encoding, errors)
        raise TypeError("Unable to decode - content not cached. Call as_bytes() first.")

    async def as_bytes(self, encoding: str = "utf-8", errors: str = "strict") -> bytes:
        """
        Return bytes representation of the value.

        This method reads the entire async iterable content and returns it as bytes.
        It generates and caches the chunks for future reuse.
        """
        # If we have cached chunks, return them joined
        if self._cached_chunks is not None:
            return b"".join(self._cached_chunks)

        # If iterator is exhausted and no cache, return empty
        if self._iter is None:
            return b""

        # Read all chunks and cache them
        chunks: list[bytes] = []

View on GitHub (pinned to d041d4d0fd)