]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Fix line splitting in `format_sse_event` to comply with SSE spec (#15515)
authorZawwar Sami <105767627+Zawwarsami16@users.noreply.github.com>
Tue, 28 Jul 2026 14:54:54 +0000 (10:54 -0400)
committerGitHub <noreply@github.com>
Tue, 28 Jul 2026 14:54:54 +0000 (16:54 +0200)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Yurii Motov <109919500+YuriiMotov@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
fastapi/sse.py
tests/test_sse.py

index 1e2bd8617161016098a4e33bd6ad0fc425091a37..642ff773407336ae1ccf1615ac9d0a7202de0061 100644 (file)
@@ -156,6 +156,12 @@ class ServerSentEvent(BaseModel):
         return self
 
 
+def _split_sse_lines(value: str) -> list[str]:
+    # Split on SSE-spec line terminators only (\n, \r\n, \r), preserving
+    # trailing empty strings.
+    return value.replace("\r\n", "\n").replace("\r", "\n").split("\n")
+
+
 def format_sse_event(
     *,
     data_str: Annotated[
@@ -206,14 +212,14 @@ def format_sse_event(
     lines: list[str] = []
 
     if comment is not None:
-        for line in comment.splitlines():
+        for line in _split_sse_lines(comment):
             lines.append(f": {line}")
 
     if event is not None:
         lines.append(f"event: {event}")
 
     if data_str is not None:
-        for line in data_str.splitlines():
+        for line in _split_sse_lines(data_str):
             lines.append(f"data: {line}")
 
     if id is not None:
index fdc85614285b47e60f6d3e21176bf02a034e42d2..e466cc0f763cb5a876ec2c7f0e9a7782d5909fbe 100644 (file)
@@ -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
 
@@ -373,6 +373,29 @@ def test_no_keepalive_when_fast(client: TestClient):
     assert ": ping\n" not in response.text
 
 
+@pytest.mark.parametrize(
+    ("data", "expected_result"),
+    [
+        ("Hello\n", b"data: Hello\ndata: \n\n"),
+        ("Hello\n\n", b"data: Hello\ndata: \ndata: \n\n"),
+        ("\n", b"data: \ndata: \n\n"),
+        ("Hello\r\nWorld", b"data: Hello\ndata: World\n\n"),
+        ("Hello\rWorld", b"data: Hello\ndata: World\n\n"),
+        ("A\u2028B", "data: A\u2028B\n\n".encode()),
+        ("A\vB", b"data: A\x0bB\n\n"),
+        ("", b"data: \n\n"),
+    ],
+)
+def test_format_sse_event_splitlines_behavior_in_data(
+    data: str, expected_result: bytes
+) -> None:
+    assert format_sse_event(data_str=data) == expected_result
+
+
+def test_format_sse_event_splitlines_behavior_in_comment():
+    assert format_sse_event(comment="hi\n") == b": hi\n: \n\n"
+
+
 # default_response_class tests