tiangolo/fastapi · error · ValueError

SSE '{field_name}' must be a single line

Error message

SSE '{field_name}' must be a single line

What it means

`ServerSentEvent` applies `AfterValidator(_check_single_line)` (sse.py:36-39, 97-122) to the `event` and `id` fields. If the value contains `\r` or `\n`, pydantic raises `ValueError`, which surfaces as a validation error when the event is constructed. SSE wire format encodes each field on a single line; embedded line breaks would corrupt the stream framing.

Source

Thrown at fastapi/sse.py:38

class EventSourceResponse(StreamingResponse):
    """Streaming response with `text/event-stream` media type.

    Use as `response_class=EventSourceResponse` on a *path operation* that uses `yield`
    to enable Server Sent Events (SSE) responses.

    Works with **any HTTP method** (`GET`, `POST`, etc.), which makes it compatible
    with protocols like MCP that stream SSE over `POST`.

    The actual encoding logic lives in the FastAPI routing layer. This class
    serves mainly as a marker and sets the correct `Content-Type`.
    """

    media_type = "text/event-stream"


def _check_single_line(v: str | None, field_name: str) -> str | None:
    if v is not None and ("\r" in v or "\n" in v):
        raise ValueError(f"SSE '{field_name}' must be a single line")
    return v


def _check_event_single_line(v: str | None) -> str | None:
    return _check_single_line(v, "event")


def _check_id_valid(v: str | None) -> str | None:
    if v is not None and "\0" in v:
        raise ValueError("SSE 'id' must not contain null characters")
    return _check_single_line(v, "id")


class ServerSentEvent(BaseModel):
    """Represents a single Server-Sent Event.

    When `yield`ed from a *path operation function* that uses
    `response_class=EventSourceResponse`, each `ServerSentEvent` is encoded

View on GitHub (pinned to 42a41db11f)

Solutions

  1. Strip/replace line breaks before assigning: `event=name.replace("\r", " ").replace("\n", " ")`.
  2. Restrict `event`/`id` to slugs/identifiers you control, not arbitrary text.
  3. Put multi-line content in `data`/`raw_data` instead, which is allowed to span lines.
  4. Add a unit test that constructs your events with the longest/edgiest inputs.

Example fix

# before
yield ServerSentEvent(event="user\nupdate", data=payload)

# after
event_name = "user_update"  # sanitize / slugify
yield ServerSentEvent(event=event_name, data=payload)
Defensive patterns

Strategy: validation

Validate before calling

def sse_single_line(value: str | None, field: str = "value") -> str | None:
    if value is not None and ("\r" in value or "\n" in value):
        raise ValueError(f"{field} must be a single line: {value!r}")
    return value

def sanitize_sse_field(value: str | None) -> str | None:
    if value is None:
        return None
    return value.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")

# usage
evt = sanitize_sse_field(user_event_name)
yield ServerSentEvent(event=evt, data=payload)

Type guard

def is_single_line(value: str | None) -> bool:
    return value is None or ("\r" not in value and "\n" not in value)

Try / catch

from fastapi.sse import ServerSentEvent

try:
    yield ServerSentEvent(event=name, data=payload)
except ValueError as exc:  # pydantic validation on construction
    logging.warning("dropping malformed SSE event: %s", exc)
    yield ServerSentEvent(event="message", data=payload)  # safe fallback

Prevention

When it happens

Trigger: Yielding `ServerSentEvent(event="user\nupdate", data=...)` or `ServerSentEvent(id="a\rb", data=...)` from an `EventSourceResponse` path operation. Passing user/DB text into `event` or `id` that happens to contain a newline.

Common situations: Using a free-form string (log line, message body, slug with a trailing newline) as an event name; reading `id` from a source that includes a line break; Windows text containing `\r\n`.

Related errors


AI-assisted analysis of tiangolo/fastapi@42a41db11f (2026-08-04). Data as JSON: /data/errors/aeae7ed34e07c0af.json. Report an issue: GitHub.