aio-libs/aiohttp · error · ValueError
Reason cannot contain \r or \n
Error message
Reason cannot contain \r or \n
What it means
StreamResponse._set_status (invoked by __init__ and set_status) rejects a reason containing \r or \n. The reason becomes the HTTP status-line reason phrase, so allowing CRLF would permit response splitting / header injection. This is the Response-side counterpart of the HTTPException guard.
Solutions
- Keep reason a short static phrase; put detail in the response body.
- Sanitize any dynamic reason: reason.replace('\r','').replace('\n','').
- Validate reason with ^[^\r\n]*$ before setting it.
Example fix
# before resp.set_status(200, str(upstream_err)) # after resp.set_status(200, 'OK') resp.text = str(upstream_err)
Defensive patterns
Strategy: validation
Validate before calling
import re
if reason is not None:
reason = re.sub(r'[\r\n]+', ' ', reason)
resp.set_status(status, 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
- Keep reason static; put detail in the body.
- Sanitize any dynamic reason by stripping CR/LF.
- Lint against CR/LF in reason literals.
When it happens
Trigger: resp.set_status(200, 'OK\nX-Inject: evil'); embedding str(exception) with newlines into the reason; templating user input into the status reason.
Common situations: Forwarding an error string verbatim as reason; multi-line text passed to set_status; copying a reason from upstream that contains CR.
Related errors
- Reason cannot contain \r or \n
- Forbidden control character detected in headers. Potential…
- 1009
- 1009
- 403: Forbidden
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/0c065654bab18b2f.
Report an issue: GitHub.
Appendix: 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 d041d4d0fd)