aio-libs/aiohttp · error · ValueError
charset must not be in content_type argument
Error message
charset must not be in content_type argument
What it means
Raised by the Response constructor when the `content_type` kwarg string itself contains the substring 'charset' (e.g. 'text/html; charset=utf-8'). aiohttp manages charset separately via the `charset` kwarg and the `; charset=...` suffix is appended internally; embedding it in content_type would produce a malformed or duplicated header.
Solutions
- Split the MIME type and charset: `Response(content_type='text/html', charset='utf-8')`.
- Use `text=` which auto-derives `text/plain; charset=utf-8` (override content_type separately if needed).
- If the string is user-supplied, parse out charset: split on ';', strip the charset param, pass the rest.
- Sanitize config values by storing MIME type and charset as separate fields.
Example fix
// before return Response(text=html, content_type='text/html; charset=utf-8') // after return Response(text=html, content_type='text/html', charset='utf-8')
Defensive patterns
Strategy: validation
Validate before calling
def split_content_type(raw: str):
if ';' in raw:
ctype, _, params = raw.partition(';')
charset = None
for p in params.split(';'):
p = p.strip()
if p.lower().startswith('charset='):
charset = p.split('=', 1)[1]
return ctype.strip(), charset
return raw, None
ctype, cs = split_content_type(raw)
return Response(text=t, content_type=ctype, charset=cs) Type guard
def content_type_has_no_charset(ctype: str) -> bool:
return 'charset' not in ctype.lower() Prevention
- Pass MIME type only to content_type=; pass charset separately via charset=.
- Parse user/config Content-Type strings before forwarding.
- Store content_type and charset as separate config fields.
- Beware copy-pasted Content-Type header values from other frameworks.
When it happens
Trigger: Passing `Response(content_type='application/json; charset=utf-8')`, or copying a full Content-Type string from a config file/header straight into the `content_type=` argument.
Common situations: Copy-pasting Content-Type header values (which legitimately contain charset) into the content_type parameter that expects only the MIME type. Configuration files that store the full header. Naive porting of Flask/Django patterns where the full header string is accepted.
Related errors
- passing both Content-Type header and content_type or…
- Setting charset for application/octet-stream doesn't make…
- body and text are not allowed together
- text argument must be str (%r)
- Cannot call write() after write_eof()
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/894b01cdfc73043c.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/web_response.py:563
status: int = 200,
reason: str | None = None,
text: str | None = None,
headers: LooseHeaders | None = None,
content_type: str | None = None,
charset: str | None = None,
zlib_executor_size: int = MAX_SYNC_CHUNK_SIZE,
zlib_executor: Executor | None = None,
) -> None:
if body is not None and text is not None:
raise ValueError("body and text are not allowed together")
if headers is None:
real_headers: CIMultiDict[str] = CIMultiDict()
else:
real_headers = CIMultiDict(headers)
if content_type is not None and "charset" in content_type:
raise ValueError("charset must not be in content_type argument")
if text is not None:
if hdrs.CONTENT_TYPE in real_headers:
if content_type or charset:
raise ValueError(
"passing both Content-Type header and "
"content_type or charset params "
"is forbidden"
)
else:
# fast path for filling headers
if not isinstance(text, str):
raise TypeError("text argument must be str (%r)" % type(text))
if content_type is None:
content_type = "text/plain"
if charset is None:
charset = "utf-8"
real_headers[hdrs.CONTENT_TYPE] = content_type + "; charset=" + charsetView on GitHub (pinned to d041d4d0fd)