aio-libs/aiohttp · warning · ValueError
Reason cannot contain \r or \n
Error message
Reason cannot contain \r or \n
What it means
Raised as ValueError by StreamResponse._set_status (aiohttp/web_response.py:160) when set_status(status, reason) is called with a reason containing '\r' or '\n'. Identical in purpose to the same check on HTTPException: aiohttp forbids CR/LF in the reason phrase because it goes into the HTTP status line and would allow response splitting. Triggered from StreamResponse (and subclasses Response, FileResponse) when changing the status after construction.
Source
Thrown at aiohttp/web_response.py:160
def reason(self) -> str:
return self._reason
def set_status(
self,
status: int,
reason: str | None = None,
) -> None:
assert (
not self.prepared
), "Cannot change the response status code after the headers have been sent"
self._set_status(status, reason)
def _set_status(self, status: int, reason: str | None) -> None:
self._status = status
if reason is None:
reason = REASON_PHRASES.get(self._status, "")
elif "\r" in reason or "\n" in reason:
raise ValueError("Reason cannot contain \\r or \\n")
self._reason = reason
@property
def keep_alive(self) -> bool | None:
return self._keep_alive
def force_close(self) -> None:
self._keep_alive = False
@property
def body_length(self) -> int:
return self._body_length
def enable_chunked_encoding(self) -> None:
"""Enables automatic chunked transfer encoding."""
if hdrs.CONTENT_LENGTH in self._headers:
raise RuntimeError(
"You can't enable chunked encoding when a content length is set"View on GitHub (pinned to c0ef574e29)
Solutions
- Strip CR/LF from any dynamic reason before calling set_status: reason.replace('\r', ' ').replace('\n', ' ').
- Use a short static reason; put dynamic detail in the body or a custom header.
- Add an assertion / lint rule that reason phrases are single-line ASCII.
Example fix
// before
resp = web.Response()
resp.set_status(200, reason=f"OK: {dynamic_msg}") # ValueError if msg has \n
# after
safe = dynamic_msg.replace("\r", " ").replace("\n", " ")
resp.set_status(200, reason=f"OK: {safe}") Defensive patterns
Strategy: validation
Validate before calling
def safe_reason(s: str, max_len: int = 200) -> str:
return s.replace("\r", " ").replace("\n", " ")[:max_len]
resp.set_status(200, reason=safe_reason(dynamic_msg)) Type guard
def is_safe_reason(s: str) -> bool:
return "\r" not in s and "\n" not in s Try / catch
try:
resp.set_status(200, reason=dynamic_msg)
except ValueError:
resp.set_status(200, reason=safe_reason(dynamic_msg)) Prevention
- Strip CR/LF from any dynamic reason phrase.
- Keep reason short and static; move detail into body or headers.
- Lint that reason phrases are single-line ASCII.
When it happens
Trigger: Calling resp.set_status(200, reason=user_text) where user_text contains a CR or LF. Often combined with dynamic reason phrases built from error messages, log lines, or template output.
Common situations: Handlers that pass exception messages or request-derived strings as reason; logging frameworks that interleave '\n' in formatted strings; tests that copy multi-line expectations into set_status.
Related errors
- Reason cannot contain \r or \n
- Method cannot contain non-token characters {method!r} (found
- Invalid Content-Length header: {content_length_hdr!r}
- compress must be one of True, False, 'deflate', or 'gzip'
- {host} - is not a canonical IPv4 address
AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04).
Data as JSON: /data/errors/0c065654bab18b2f.json.
Report an issue: GitHub.