aio-libs/aiohttp · error · ValueError

To decode nested multipart you need to use custom reader

Error message

To decode nested multipart you need to use custom reader

What it means

BaseRequest.post() only handles a single level of multipart/form-data. If a part is itself a MultipartReader (nested multipart, e.g. multipart/mixed or message/rfc822 inside form-data), it raises ValueError directing you to use a custom reader. post() will not recurse into nested multipart.

Solutions

  1. Use request.multipart() directly and walk the reader yourself to handle nested parts.
  2. Do not call post() for nested multipart; write a dedicated parser.
  3. Reject nested multipart with 400 if your API does not support it.

Example fix

# before
data = await request.post()  # raises on nested multipart
# after
reader = await request.multipart()
async for part in reader:
    if isinstance(part, MultipartReader):
        # handle nested multipart manually
        ...
Defensive patterns

Strategy: validation

Validate before calling

if request.content_type == 'multipart/form-data':
    # peek for nested multipart before calling post()
    reader = await request.multipart()
    # handle nested parts manually

Type guard

def needs_custom_reader(part: object) -> bool:
    return not isinstance(part, BodyPartReader)

Try / catch

try:
    data = await request.post()
except ValueError:
    # fall back to a custom multipart reader
    reader = await request.multipart()

Prevention

When it happens

Trigger: A multipart/form-data body containing a nested multipart/* part (aggregate payloads, email-style submissions, RFC 7578 nested examples).

Common situations: Clients composing multiple files under one field with multipart/mixed; email gateways; aggregate document uploads.

Related errors


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

Appendix: source

Thrown at aiohttp/web_request.py:802

                        raw_data = bytearray()
                        while chunk := await field.read_chunk():
                            size += len(chunk)
                            if 0 < max_size < size:
                                raise HTTPRequestEntityTooLarge(max_size)
                            raw_data.extend(chunk)

                        value = bytearray()
                        # form-data doesn't support compression, so don't need to check size again.
                        async for d in field.decode_iter(raw_data):  # type: ignore[arg-type]
                            value.extend(d)

                        if field_ct is None or field_ct.startswith("text/"):
                            charset = field.get_charset(default="utf-8")
                            out.add(field.name, value.decode(charset))
                        else:
                            out.add(field.name, value)  # type: ignore[arg-type]
                else:
                    raise ValueError(
                        "To decode nested multipart you need to use custom reader",
                    )
        else:
            data = await self.read()
            if data:
                charset = self.charset or "utf-8"
                bytes_query = data.rstrip()
                try:
                    query = bytes_query.decode(charset)
                except (LookupError, UnicodeDecodeError):
                    raise HTTPUnsupportedMediaType()
                out.extend(
                    parse_qsl(qs=query, keep_blank_values=True, encoding=charset)
                )

        self._post = MultiDictProxy(out)
        return self._post

View on GitHub (pinned to d041d4d0fd)