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
- Strip null bytes: `id = raw_id.replace("\0", "")` before constructing the event.
- Reject/sanitize upstream inputs that may contain control characters.
- 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
- Never pass binary/C-string buffers or raw external tokens as an SSE id.
- Prefer generated ids (uuid/int) for events.
- Sanitize control characters at the trust boundary, not at SSE construction.
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
- SSE '{field_name}' must be a single line
- Cannot set both 'data' and 'raw_data' on the same ServerSent
- {"errors": exc.errors(), "body": body.decode()}
- {"errors": exc.errors(), "body": body.decode()}
- Invalid YAML
AI-assisted analysis of tiangolo/fastapi@42a41db11f (2026-08-04).
Data as JSON: /data/errors/ace7ef806e04f036.json.
Report an issue: GitHub.