aio-libs/aiohttp · error · TypeError

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

Error message

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

What it means

Raised by AsyncIterablePayload.decode() when the payload has not yet been materialized into a cache. AsyncIterablePayload streams lazily and only populates _cached_chunks after as_bytes() runs; decode() is a synchronous helper that needs that cache to exist. Calling decode() on a fresh or write-only payload therefore fails. The fix is to call the async as_bytes() first, which populates the cache and makes decode() reusable.

Source

Thrown at aiohttp/payload.py:1075

                    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
            self._consumed = True  # Mark as consumed when streamed without caching

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

Solutions

  1. Call `await payload.as_bytes()` first to populate the cache, then payload.decode() works.
  2. If you only need the string, use `data = await payload.as_bytes(); text = data.decode('utf-8')` directly.
  3. Avoid decode() for streaming payloads you intend to also send — materializing consumes the iterator.
  4. Use BytesPayload instead if you need synchronous decode() support from the start.

Example fix

// before
text = payload.decode('utf-8')  # raises TypeError
// after
data = await payload.as_bytes()
text = data.decode('utf-8')
Defensive patterns

Strategy: validation

Validate before calling

# Always materialize before sync decode:
if payload._cached_chunks is None:
    data = await payload.as_bytes()  # populates cache
text = payload.decode() if payload._cached_chunks is not None else data.decode()

Try / catch

try:
    text = payload.decode()
except TypeError:
    data = await payload.as_bytes()
    text = data.decode('utf-8')

Prevention

When it happens

Trigger: Calling payload.decode() directly on an AsyncIterablePayload (or StreamReaderPayload) that was streamed via write() without ever calling as_bytes(). Reproducible when a test or middleware tries to log the body as text after the payload was consumed by the writer.

Common situations: Debug logging that wants to peek at the body; tests that assert on payload content; middleware that reads body for inspection; using decode() (sync) instead of as_bytes() (async) on a streaming payload.

Related errors


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