diff --git a/fastapi/sse.py b/fastapi/sse.py index 1e2bd86171..20d8659e07 100644 --- a/fastapi/sse.py +++ b/fastapi/sse.py @@ -88,7 +88,8 @@ class ServerSentEvent(BaseModel): Use this when you need to send pre-formatted text, HTML fragments, CSV lines, or any non-JSON payload. The string is placed directly - into the `data:` field as-is. + into the `data:` field as-is. An empty string still emits a single + empty `data:` line. Mutually exclusive with `data`. """ @@ -213,7 +214,11 @@ def format_sse_event( lines.append(f"event: {event}") if data_str is not None: - for line in data_str.splitlines(): + # Normalize line endings and preserve empty data lines. + # This keeps explicit empty payloads (`data_str=""`) and trailing + # newlines represented as `data:` lines in SSE wire format. + normalized_data = data_str.replace("\r\n", "\n").replace("\r", "\n") + for line in normalized_data.split("\n"): lines.append(f"data: {line}") if id is not None: diff --git a/tests/test_sse.py b/tests/test_sse.py index 86a67f8f9f..f898644cb0 100644 --- a/tests/test_sse.py +++ b/tests/test_sse.py @@ -6,7 +6,7 @@ import fastapi.routing import pytest from fastapi import APIRouter, FastAPI from fastapi.responses import EventSourceResponse -from fastapi.sse import ServerSentEvent +from fastapi.sse import ServerSentEvent, format_sse_event from fastapi.testclient import TestClient from pydantic import BaseModel @@ -264,6 +264,18 @@ def test_data_and_raw_data_mutually_exclusive(): ServerSentEvent(data="json", raw_data="raw") +def test_format_sse_event_keeps_empty_data_line(): + """An explicit empty payload should emit one `data:` line.""" + payload = format_sse_event(data_str="") + assert payload == b"data: \n\n" + + +def test_format_sse_event_normalizes_crlf_and_keeps_trailing_empty_line(): + """CRLF and trailing newline should produce valid SSE data lines.""" + payload = format_sse_event(data_str="first\r\nsecond\r\n") + assert payload == b"data: first\ndata: second\ndata: \n\n" + + def test_sse_on_router_included_in_app(client: TestClient): response = client.get("/api/events") assert response.status_code == 200