Nobody has started this yet — be first.
Business impact
The Server-Sent Events spec only dispatches an event to the client's EventSource.onmessage once a blank line terminates it. If that blank line is missing, a strict SSE parser keeps buffering fields, waiting for the terminator, and only dispatches once something else eventually supplies one -- so every notification arrives one event late, and if a user's session ends after their last notification, that final notification may never be dispatched to the browser at all. For a notifications feature, the last thing that happened being the one thing the user never finds out about is close to the worst possible failure mode.
Problem
format_sse builds each SSE chunk by joining its lines with a single newline and appending one more newline at the end, producing exactly one trailing newline -- one short of the blank line (two consecutive newlines) the SSE spec requires to terminate an event.
Current behavior
format_sse('1-0', 'message', '{}') returns a string ending in a single "\n", not "\n\n" -- chunk.endswith("\n\n") is False for every event it formats, even though HEARTBEAT_COMMENT a few lines above it correctly ends in "\n\n".
Expected behavior
Every chunk format_sse returns must end in a genuine blank line -- two consecutive newline characters -- so each event is self-terminating on the wire, exactly like HEARTBEAT_COMMENT already is.
Steps to reproduce
cd fastapi/sse_notificationspython3 -c "from app.streaming import format_sse; c = format_sse('1-0', 'message', '{}'); print(repr(c)); print(c.endswith(chr(10)*2))"False -- the chunk ends in one newline, not the required blank-line terminator.Why this matters
This is the kind of bug a substring-based test (asserting "id: X" in chunk) will never catch, because the field content is completely correct -- only the terminator is missing. It takes either a strict byte-for-byte check of the chunk's ending, or an actual SSE-parsing client, to notice anything is wrong. HEARTBEAT_COMMENT right above this function gets the terminator right, which makes the asymmetry easy to miss on a quick read.
Suggested approach
Compare format_sse's return value against HEARTBEAT_COMMENT's literal ending. "\n".join(lines) only puts a newline between elements -- think about what belongs at the end of lines for the join to naturally produce a trailing blank line.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_04_sse_event_terminator.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b fix/sse-missing-blank-line-terminatorFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/sse-missing-blank-line-terminatorSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inhttp://localhost:8000http://localhost:8001Authorization: Bearer <user_id>X-User-Id: <user_id>curl localhost:8000/healthz
curl -N -H "Authorization: Bearer alice" localhost:8000/notifications/stream
# watch `: heartbeat` lines arrive roughly every 15s with no other traffic
Work the tickets in practice_tickets/tickets/ (ordered easy → hard); each names one dedicated pytest test in practice_tickets/tests/:
pytest practice_tickets/tests/test_ticket_01_heartbeat_default_margin.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
Tickets 14 and 15 need genuinely concurrent, long-lived streams, which the ASGI test transport used everywhere else in this project can't represent -- their tests spin up a real (short-lived) uvicorn server on a loopback socket instead (LiveServer in practice_tickets/tests/conftest.py).
Level 1
Fix a bug
Read existing behaviour, correct it.