tiangolo/fastapi · error · ValueError
SSE 'id' must not contain null characters
Error message
SSE 'id' must not contain null characters
What it means
Raised by `_check_id_valid` (fastapi/sse.py:48) as a ValueError when the `id` field of a `ServerSentEvent` contains a null character ('\0'). The SSE spec forbids NUL in event IDs because they break `Last-Event-ID` handling on reconnect; the browser sends the id back as a header where NUL is illegal. This is checked in addition to the single-line rule.
Solutions
- Strip NUL bytes: `id = raw_id.replace('\0', '')` before constructing the event.
- Encode binary ids as hex/base64 so they contain no NUL: `id = raw_bytes.hex()`.
- Validate upstream that ids are printable ASCII/UTF-8 without NUL.
Example fix
// before
yield ServerSentEvent(id=raw_bytes.decode('latin-1'), data=payload)
// after
yield ServerSentEvent(id=raw_bytes.hex(), data=payload) Defensive patterns
Strategy: validation
Validate before calling
from fastapi.sse import ServerSentEvent
def safe_sse_id(raw) -> str | None:
if raw is None:
return None
s = str(raw).replace('\0', '')
s = s.replace('\r', '').replace('\n', ' ')
return s
yield ServerSentEvent(id=safe_sse_id(raw_id), data=payload) Type guard
def id_has_no_null(value: object) -> bool:
return isinstance(value, str) and '\0' not in value Prevention
- Encode binary identifiers as hex/base64 before using as SSE id.
- Strip NUL from any string sourced from binary/interop.
- Reject ids containing control characters at the input boundary.
When it happens
Trigger: Constructing `ServerSentEvent(id='abc\x00def', data=...)`. Building the id from binary/serialized data that contains NUL bytes. Using a database/protobuf value that includes embedded nulls as the SSE id.
Common situations: Feeding binary identifiers (e.g. packed structs, UUIDs from binary sources) into the `id` field. Logging/telemetry payloads mistakenly placed in `id`. Strings from C-interop or NUL-padded buffers.
Related errors
- Cannot set both 'data' and 'raw_data' on the same…
- SSE ' ' must be a single line
- Code block (lines - ) has different language than the…
- {"errors": exc.errors(), "body": body.decode()}
- {"errors": exc.errors(), "body": body.decode()}
AI-assisted analysis of tiangolo/fastapi@3e8d1526d8 (2026-08-11).
Data as JSON: /api/errors/ace7ef806e04f036.
Report an issue: GitHub.
Appendix: 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 3e8d1526d8)