aio-libs/aiohttp · error · ValueError

Unsupported body type %r

Error message

Unsupported body type %r

What it means

Raised by the Response.body setter when the value is not None, not bytes/bytearray, and not registered in aiohttp.payload.PAYLOAD_REGISTRY. The registry maps Python types (str, dict via JsonPayload, file objects via StreamReader, etc.) to Payload adapters; anything not registered is rejected as unsupported rather than silently coerced.

Solutions

  1. Serialize to bytes first: `resp.body = json.dumps(obj).encode()` or `orjson.dumps(obj)`.
  2. Use `text=` for str payloads, `body=` for bytes, `json_response()` for dict/list objects.
  3. Register a custom Payload adapter via `aiohttp.payload.PAYLOAD_REGISTRY.register(MyType, MyPayload)` if you need first-class support.
  4. Wrap file-like objects in `aiohttp.streamer` or use `web.FileResponse` for filesystem paths.

Example fix

// before
resp.body = my_dataclass_instance  # LookupError -> ValueError

// after
resp.body = json.dumps(asdict(my_dataclass_instance)).encode('utf-8')
Defensive patterns

Strategy: type-guard

Validate before calling

def to_body_bytes(value):
    if isinstance(value, (bytes, bytearray)):
        return bytes(value)
    if isinstance(value, str):
        return value.encode('utf-8')
    # fall back to JSON for dict/list/dataclass-like objects
    return json.dumps(value).encode('utf-8')

resp.body = to_body_bytes(value)

Type guard

import bytes as _

def is_supported_body(value) -> bool:
    return value is None or isinstance(value, (bytes, bytearray, str, dict, list, tuple))

Try / catch

try:
    resp.body = value
except ValueError:
    resp.body = json.dumps(value).encode('utf-8')

Prevention

When it happens

Trigger: Assigning `resp.body = 42`, a custom class instance, a `pathlib.Path`, a `memoryview` of an unsupported backing type, or any object whose type has no registered Payload adapter. Also triggered when a Payload subclass is registered but the value is a sibling type.

Common situations: Returning ORM model instances, dataclasses, or Pydantic objects directly as body without serializing. Passing a generator/async generator that isn't wrapped in a StreamingResponse-like payload. Forgetting that str needs to go through text= (str IS registered but the error fires for truly unknown types).

Related errors


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

Appendix: source

Thrown at aiohttp/web_response.py:620

        self._zlib_executor_size = zlib_executor_size
        self._zlib_executor = zlib_executor

    @property
    def body(self) -> bytes | bytearray | Payload | None:
        return self._body

    @body.setter
    def body(self, body: Any) -> None:
        if body is None:
            self._body = None
        elif isinstance(body, (bytes, bytearray)):
            self._body = body
        else:
            try:
                self._body = body = payload.PAYLOAD_REGISTRY.get(body)
            except payload.LookupError:
                raise ValueError("Unsupported body type %r" % type(body))

            headers = self._headers

            # set content-type
            if hdrs.CONTENT_TYPE not in headers:
                headers[hdrs.CONTENT_TYPE] = body.content_type

            # copy payload headers
            if body.headers:
                for key, value in body.headers.items():
                    if key not in headers:
                        headers[key] = value

        self._compressed_body = None

    @property
    def text(self) -> str | None:
        if self._body is None:

View on GitHub (pinned to d041d4d0fd)