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

  1. Keep reason a short static phrase; put detail in the text/body argument.
  2. Sanitize any dynamic reason: reason = reason.replace('\r','').replace('\n','').
  3. 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

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


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)