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 the constructor of every HTTP exception (HTTPException.__init__ in aiohttp/web_exceptions.py:103) when you pass reason=... that contains a carriage return (\r) or newline (\n). aiohttp forbids these characters because reason goes verbatim into the HTTP status line (e.g. 'HTTP/1.1 418 <reason>'), and an embedded CRLF would let you inject a second HTTP response (HTTP response splitting). The check is explicit and unconditional.
Source
Thrown at aiohttp/web_exceptions.py:103
# You should set in subclasses:
# status = 200
status_code = -1
empty_body = False
default_reason = "" # Initialized at the end of the module
def __init__(
self,
*,
headers: LooseHeaders | None = None,
reason: str | None = None,
text: str | None = None,
content_type: str | None = None,
) -> None:
if reason is None:
reason = self.default_reason
elif "\r" in reason or "\n" in reason:
raise ValueError("Reason cannot contain \\r or \\n")
if text is None:
if not self.empty_body:
text = f"{self.status_code}: {reason}"
else:
if self.empty_body:
warnings.warn(
f"text argument is deprecated for HTTP status {self.status_code} "
"since 4.0 and scheduled for removal in 5.0 (#3462),"
"the response should be provided without a body",
DeprecationWarning,
stacklevel=2,
)
if headers is not None:
real_headers = CIMultiDict(headers)
else:
real_headers = CIMultiDict()View on GitHub (pinned to c0ef574e29)
Solutions
- Strip or reject CR/LF in the reason string before constructing the exception: reason.replace('\r', ' ').replace('\n', ' ').
- Keep reason as a short, static phrase; put dynamic detail in text= or a custom header instead.
- Validate user-supplied strings at the trust boundary, not at HTTP-exception construction time.
- Add a unit test asserting reason contains no control characters for any user-influenced value.
Example fix
// before
raise HTTPBadRequest(reason=f"Invalid field: {user_input}")
# after
safe = user_input.replace("\r", " ").replace("\n", " ")[:200]
raise HTTPBadRequest(text=f"Invalid field: {user_input}") Defensive patterns
Strategy: validation
Validate before calling
def safe_reason(s: str, max_len: int = 200) -> str:
if "\r" in s or "\n" in s:
s = s.replace("\r", " ").replace("\n", " ")
return s[:max_len] Type guard
def is_safe_reason(s: str) -> bool:
return "\r" not in s and "\n" not in s and len(s) <= 200 Try / catch
try:
raise HTTPBadRequest(reason=user_text)
except ValueError:
raise HTTPBadRequest(text=user_text) Prevention
- Never put dynamic, multi-line, or user-supplied text in reason=.
- Keep reason phrases short and static; move detail to text= or headers.
- Add a unit test asserting no control chars in any user-influenced reason.
When it happens
Trigger: Constructing any web_exceptions class with reason containing a CR or LF: HTTPBadRequest(reason='oops\nX-Evil: 1'), HTTPException subclasses, or returning an HTTPException(reason=user_input) where user_input is unvalidated. Both '\r' and '\n' (Unix LF) trigger it, in either order.
Common situations: Passing exception/Status text derived from a user-controlled field (error message, form input, upstream API response) straight into reason=. Also happens when logging or copy-pasting multi-line strings as the reason. Sometimes surfaces after a refactor that started forwarding raw error bodies as the HTTP reason phrase.
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/1254d5e9656baba8.json.
Report an issue: GitHub.