aio-libs/aiohttp · error · RuntimeError
Invalid default charset
Error message
Invalid default charset
What it means
Raised by MultipartReader.next() when processing a multipart/form-data body that uses the _charset_ convention (RFC 7578 §4.6): the first part named '_charset_' declares the default charset for subsequent parts. aiohttp reads up to 32 bytes for that value; if more than 31 bytes are read it treats the charset as invalid/malicious and raises RuntimeError.
Source
Thrown at aiohttp/multipart.py:771
await self._read_boundary()
if self._at_eof: # we just read the last boundary, nothing to do there
# https://github.com/python/mypy/issues/17537
return None # type: ignore[unreachable]
part = await self.fetch_next_part()
# https://datatracker.ietf.org/doc/html/rfc7578#section-4.6
if (
self._last_part is None
and self._mimetype.subtype == "form-data"
and isinstance(part, BodyPartReader)
):
_, params = parse_content_disposition(part.headers.get(CONTENT_DISPOSITION))
if params.get("name") == "_charset_":
# Longest encoding in https://encoding.spec.whatwg.org/encodings.json
# is 19 characters, so 32 should be more than enough for any valid encoding.
charset = await part.read_chunk(32)
if len(charset) > 31:
raise RuntimeError("Invalid default charset")
self._default_charset = charset.strip().decode()
part = await self.fetch_next_part()
self._last_part = part
return self._last_part
async def release(self) -> None:
"""Reads all the body parts to the void till the final boundary."""
while not self._at_eof:
item = await self.next()
if item is None:
break
await item.release()
async def fetch_next_part(
self,
) -> Union["MultipartReader", BodyPartReader]:
"""Returns the next body part reader."""
headers = await self._read_headers()View on GitHub (pinned to c0ef574e29)
Solutions
- Reject the request with 400 — a >31-byte _charset_ value is not legitimate.
- Fix the client so the _charset_ part contains a real, short encoding name (e.g. 'utf-8').
- Wrap multipart parsing in try/except RuntimeError and treat it as a bad request.
Example fix
// before
async for part in request.multipart(): # _charset_ too long -> RuntimeError
...
// after
try:
async for part in request.multipart():
...
except RuntimeError:
return web.Response(status=400, text='malformed multipart form')
Defensive patterns
Strategy: try-catch
Try / catch
try:
async for part in request.multipart():
process(part)
except RuntimeError as e:
if 'Invalid default charset' in str(e):
return web.Response(status=400, text='malformed _charset_ field')
raise Prevention
- Treat multipart parsing failures as 400 by default.
- Log _charset_ values during incidents to spot malicious clients.
- Rate-limit multipart endpoints to blunt header/charset bombs.
When it happens
Trigger: Receiving a multipart/form-data request whose first part is named '_charset_' but whose body is longer than 31 bytes. This is almost always malformed or malicious input, since real charset names are short (longest in the WHATWG list is ~19 chars).
Common situations: Fuzzing/malicious clients; a buggy form generator that puts the wrong content into the _charset_ field; a client confusing the _charset_ field with an actual text value.
Related errors
- data cannot be decoded with %s encoding
- Multipart field missing name.
- Method cannot contain non-token characters {method!r} (found
- {host} - is not a canonical IPv4 address
- filename must be an instance of str. Got: %s
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/1aa98f9eb00ca4f1.json.
Report an issue: GitHub.