{"id":"1254d5e9656baba8","repo":"aio-libs/aiohttp","slug":"reason-cannot-contain-r-or-n","errorCode":null,"errorMessage":"Reason cannot contain \\r or \\n","messagePattern":"Reason cannot contain \\\\r or \\\\n","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"aiohttp/web_exceptions.py","lineNumber":103,"sourceCode":"    # You should set in subclasses:\n    # status = 200\n\n    status_code = -1\n    empty_body = False\n    default_reason = \"\"  # Initialized at the end of the module\n\n    def __init__(\n        self,\n        *,\n        headers: LooseHeaders | None = None,\n        reason: str | None = None,\n        text: str | None = None,\n        content_type: str | None = None,\n    ) -> None:\n        if reason is None:\n            reason = self.default_reason\n        elif \"\\r\" in reason or \"\\n\" in reason:\n            raise ValueError(\"Reason cannot contain \\\\r or \\\\n\")\n\n        if text is None:\n            if not self.empty_body:\n                text = f\"{self.status_code}: {reason}\"\n        else:\n            if self.empty_body:\n                warnings.warn(\n                    f\"text argument is deprecated for HTTP status {self.status_code} \"\n                    \"since 4.0 and scheduled for removal in 5.0 (#3462),\"\n                    \"the response should be provided without a body\",\n                    DeprecationWarning,\n                    stacklevel=2,\n                )\n\n        if headers is not None:\n            real_headers = CIMultiDict(headers)\n        else:\n            real_headers = CIMultiDict()","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/web_exceptions.py#L85-L121","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nraise HTTPBadRequest(reason=f\"Invalid field: {user_input}\")\n\n# after\nsafe = user_input.replace(\"\\r\", \" \").replace(\"\\n\", \" \")[:200]\nraise HTTPBadRequest(text=f\"Invalid field: {user_input}\")","handlingStrategy":"validation","validationCode":"def safe_reason(s: str, max_len: int = 200) -> str:\n    if \"\\r\" in s or \"\\n\" in s:\n        s = s.replace(\"\\r\", \" \").replace(\"\\n\", \" \")\n    return s[:max_len]","typeGuard":"def is_safe_reason(s: str) -> bool:\n    return \"\\r\" not in s and \"\\n\" not in s and len(s) <= 200","tryCatchPattern":"try:\n    raise HTTPBadRequest(reason=user_text)\nexcept ValueError:\n    raise HTTPBadRequest(text=user_text)","preventionTips":["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."],"tags":["http","security","response-splitting","validation"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}