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

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

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


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)