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 StreamResponse._set_status (aiohttp/web_response.py:160) when set_status(status, reason) is called with a reason containing '\r' or '\n'. Identical in purpose to the same check on HTTPException: aiohttp forbids CR/LF in the reason phrase because it goes into the HTTP status line and would allow response splitting. Triggered from StreamResponse (and subclasses Response, FileResponse) when changing the status after construction.

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 c0ef574e29)

Solutions

  1. Strip CR/LF from any dynamic reason before calling set_status: reason.replace('\r', ' ').replace('\n', ' ').
  2. Use a short static reason; put dynamic detail in the body or a custom header.
  3. Add an assertion / lint rule that reason phrases are single-line ASCII.

Example fix

// before
resp = web.Response()
resp.set_status(200, reason=f"OK: {dynamic_msg}")  # ValueError if msg has \n

# after
safe = dynamic_msg.replace("\r", " ").replace("\n", " ")
resp.set_status(200, reason=f"OK: {safe}")
Defensive patterns

Strategy: validation

Validate before calling

def safe_reason(s: str, max_len: int = 200) -> str:
    return s.replace("\r", " ").replace("\n", " ")[:max_len]

resp.set_status(200, reason=safe_reason(dynamic_msg))

Type guard

def is_safe_reason(s: str) -> bool:
    return "\r" not in s and "\n" not in s

Try / catch

try:
    resp.set_status(200, reason=dynamic_msg)
except ValueError:
    resp.set_status(200, reason=safe_reason(dynamic_msg))

Prevention

When it happens

Trigger: Calling resp.set_status(200, reason=user_text) where user_text contains a CR or LF. Often combined with dynamic reason phrases built from error messages, log lines, or template output.

Common situations: Handlers that pass exception messages or request-derived strings as reason; logging frameworks that interleave '\n' in formatted strings; tests that copy multi-line expectations into set_status.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/0c065654bab18b2f.json. Report an issue: GitHub.