aio-libs/aiohttp · error · ValueError
Reason cannot contain \r or \n
Error message
Reason cannot contain \r or \n
What it means
HTTPException.__init__ (and all subclasses such as HTTPForbidden, HTTPNotFound) reject a reason containing \r or \n. The reason becomes the HTTP status-line reason phrase, so allowing CRLF would permit HTTP response splitting / header injection. The guard runs whenever an explicit reason is supplied.
Solutions
- Keep reason a short static phrase; put detail in the text/body argument.
- Sanitize any dynamic reason: reason = reason.replace('\r','').replace('\n','').
- Validate reason with a regex (^[^\r\n]*$) before constructing the exception.
Example fix
# before raise HTTPForbidden(reason=str(upstream_err)) # after raise HTTPForbidden(text=str(upstream_err))
Defensive patterns
Strategy: validation
Validate before calling
import re
if reason is not None and re.search(r'[\r\n]', reason):
reason = re.sub(r'[\r\n]+', ' ', reason)
raise web.HTTPForbidden(reason=reason) Type guard
def is_safe_reason(r: str | None) -> TypeGuard[str]:
return r is not None and '\r' not in r and '\n' not in r Prevention
- Never put user input or exception text into the reason phrase.
- Use the text= argument for detail, reason= for short static phrases.
- Run a CI lint that rejects CR/LF in any literal reason string.
When it happens
Trigger: HTTPForbidden(reason='denied\nX-Injected: evil'); embedding str(some_exception) (which contains newlines) into reason; templating user input into the reason phrase.
Common situations: Forwarding an upstream error message verbatim as reason; multi-line validation messages passed as reason; logging text reused for the status line.
Related errors
- Forbidden control character detected in headers. Potential…
- Reason cannot contain \r or \n
- Bad HTTP method in status line
- Duplicate ' ' header found.
- Method cannot contain non-token characters
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/1254d5e9656baba8.
Report an issue: GitHub.
Appendix: 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 d041d4d0fd)