tiangolo/fastapi · error · ValueError

SSE 'id' must not contain null characters

Error message

SSE 'id' must not contain null characters

What it means

`ServerSentEvent.id` uses `AfterValidator(_check_id_valid)` (sse.py:46-49) which raises `ValueError` if the id contains a null byte (`\0`). The SSE spec forbids U+0000 in the event id; it also still applies the single-line check. The browser sends `id` back as `Last-Event-ID` on reconnect, and a null byte would break that round-trip.

Source

Thrown at fastapi/sse.py:48

    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
    into the [SSE wire format](https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream)
    (`text/event-stream`).

    If you yield a plain object (dict, Pydantic model, etc.) instead, it is
    automatically JSON-encoded and sent as the `data:` field.

    All `data` values **including plain strings** are JSON-serialized.

    For example, `data="hello"` produces `data: "hello"` on the wire (with
    quotes).

View on GitHub (pinned to 42a41db11f)

Solutions

  1. Strip null bytes: `id = raw_id.replace("\0", "")` before constructing the event.
  2. Reject/sanitize upstream inputs that may contain control characters.
  3. Use a generated, known-safe id (uuid, integer) instead of passing raw external values.

Example fix

# before
yield ServerSentEvent(id=row_id, data=payload)  # row_id may contain \0

# after
clean_id = (row_id or "").replace("\0", "")
yield ServerSentEvent(id=clean_id, data=payload)
Defensive patterns

Strategy: validation

Validate before calling

def sanitize_sse_id(value: str | None) -> str | None:
    if value is None:
        return None
    if "\0" in value:
        raise ValueError(f"SSE id contains null byte: {value!r}")
    if "\r" in value or "\n" in value:
        raise ValueError(f"SSE id must be single line: {value!r}")
    return value

# usage (strip-and-accept variant):
yield ServerSentEvent(id=(raw or "").replace("\0", ""), data=payload)

Type guard

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

Try / catch

from fastapi.sse import ServerSentEvent

try:
    yield ServerSentEvent(id=raw_id, data=payload)
except ValueError as exc:
    logging.warning("dropping SSE event with bad id: %s", exc)
    yield ServerSentEvent(id=None, data=payload)  # omit id on bad input

Prevention

When it happens

Trigger: Yielding `ServerSentEvent(id=some_id, data=...)` where `some_id` contains `\0` — e.g. a binary/C-string buffer, a DB value with an embedded null, or untrusted input not sanitized.

Common situations: Using a row id or token that came from a binary source; reading headers/bytes and passing them through as an event id; corrupted data containing control characters.

Related errors


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