Deep Dive: SSE Data Serialization
In this project, Server-Sent Events (SSE) are implemented with a focus on flexible data serialization. While standard FastAPI responses typically return a single JSON object, SSE requires a stream of specifically formatted text blocks. The implementation in fastapi/sse.py and the integration in fastapi/routing.py provide two distinct paths for serializing this data: automatic JSON wrapping for simple use cases and the ServerSentEvent class for fine-grained control.
The Serialization Pipeline
The serialization process is triggered when a path operation function uses response_class=EventSourceResponse. This class acts as a marker for the FastAPI router, which then wraps the returned generator in SSE-specific encoding logic.
The core of this logic resides in fastapi/routing.py within the _serialize_sse_item helper function. This function determines how to transform a yielded object into the SSE wire format based on its type.
Automatic JSON Serialization
For many use cases, developers can yield plain Python objects (such as dictionaries or Pydantic models) directly. When the router encounters a non-ServerSentEvent object, it automatically:
- Serializes the object to JSON using
jsonable_encoder. - Wraps the resulting JSON string in the
data:field format.
As seen in tests/test_sse.py, yielding a Pydantic model like Item(name="Plumbus") results in the following wire format:
data: {"name":"Plumbus"}
Explicit Control with ServerSentEvent
When metadata like event types (event), unique identifiers (id), or reconnection hints (retry) are required, the ServerSentEvent class is used. This class is a Pydantic model that explicitly defines the structure of an SSE message.
# From fastapi/sse.py
class ServerSentEvent(BaseModel):
data: Any = None
raw_data: str | None = None
event: str | None = None
id: str | None = None
retry: int | None = None
comment: str | None = None
Data vs. Raw Data
A critical design choice in this implementation is the distinction between the data and raw_data fields. These fields are mutually exclusive, enforced by a @model_validator in the ServerSentEvent class.
The data Field (JSON-First)
The data field is intended for structured data. The implementation ensures that anything passed to data is JSON-serialized. This includes plain strings, which results in them being quoted on the wire.
For example, yielding ServerSentEvent(data="hello") produces:
data: "hello"
This behavior ensures consistency when the client-side EventSource listener expects to run JSON.parse() on the incoming message.
The raw_data Field (The Escape Hatch)
The raw_data field allows developers to bypass JSON serialization entirely. This is essential for sending pre-formatted text, HTML fragments, or CSV data where JSON quotes would break the payload.
As demonstrated in tests/test_sse.py:
# Yielding raw_data
yield ServerSentEvent(raw_data="plain text without quotes")
Produces:
data: plain text without quotes
Wire Format Construction
The final transformation from a ServerSentEvent instance to bytes is handled by format_sse_event in fastapi/sse.py. This utility function iterates through the fields and constructs the multi-line string required by the SSE specification.
One notable detail is the handling of multi-line data. The SSE spec requires that each line of a multi-line payload be prefixed with data: . The format_sse_event function handles this automatically by splitting the input string:
# From fastapi/sse.py
if data_str is not None:
for line in data_str.splitlines():
lines.append(f"data: {line}")
Keep-Alive and Timeouts
To prevent proxies and load balancers from closing idle connections, the implementation includes an automatic keep-alive mechanism.
In fastapi/routing.py, a background task monitors the stream. If the generator does not yield an item within the _PING_INTERVAL (defaulting to 15 seconds), the server automatically injects a "ping" comment:
# From fastapi/sse.py
KEEPALIVE_COMMENT = b": ping\n\n"
_PING_INTERVAL: float = 15.0
This comment is ignored by the browser's EventSource API but keeps the underlying TCP connection active.
Constraints and Validation
The implementation enforces several constraints to ensure compatibility with the SSE specification:
- ID Safety: The
idfield is validated via_check_id_no_nullto ensure it contains no null characters (\0), which are prohibited in the SSE wire format. - Retry Logic: The
retryfield must be a non-negative integer representing milliseconds. - Exclusivity: A
ValueErroris raised if bothdataandraw_dataare provided, forcing a clear choice between JSON-managed or manual serialization.