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
The content_type argument must be a bare media type (e.g. 'application/json'); aiohttp builds the full Content-Type header including charset itself (line 581, 593). Embedding 'charset=' in content_type double-sets it and is rejected at line 562-563.
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 c0ef574e29)
Solutions
- Pass content_type='text/html' and charset='utf-8' separately.
- Drop the charset segment entirely and let aiohttp default it.
- If you need full control, pass headers={'Content-Type': '...'} and omit content_type/charset.
Example fix
# before resp = Response(text=html, content_type='text/html; charset=utf-8') # raises ValueError # after resp = Response(text=html, content_type='text/html', charset='utf-8')
Defensive patterns
Strategy: validation
Validate before calling
def clean_content_type(ct: str | None) -> str | None:
if ct and 'charset' in ct.lower():
# split off charset; let aiohttp re-add it
return ct.split(';')[0].strip()
return ct Type guard
def content_type_is_bare(ct: str | None) -> bool:
return ct is None or (';' not in ct and 'charset' not in ct.lower()) Prevention
- Pass content_type and charset as separate kwargs.
- Never paste a full 'type; charset=...' string into content_type.
- Use headers={'Content-Type': full_value} only when you need full manual control.
When it happens
Trigger: Calling Response(content_type='text/html; charset=utf-8', ...) or json_response(content_type='application/json; charset=utf-8'). The substring check is 'charset' in content_type.
Common situations: Copy-pasting a full Content-Type header value into the content_type param; building content_type dynamically and accidentally concatenating '; charset=...'.
Related errors
- Setting charset for application/octet-stream doesn't make se
- passing both Content-Type header and content_type or charset
- Got more than {limit} bytes when reading: {line!r}.
- Unsupported type for last_modified: {type(value).__name__}
- Unsupported etag type: {type(value)}. etag must be str, ETag
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/894b01cdfc73043c.json.
Report an issue: GitHub.