Skip to content

🐛 Fix response_class=EventSourceResponse sending an empty body for non-generator endpoints - #16192

Open
SEPURI-SAI-KRISHNA wants to merge 1 commit into
fastapi:masterfrom
SEPURI-SAI-KRISHNA:fix-sse-non-generator-dispatch
Open

🐛 Fix response_class=EventSourceResponse sending an empty body for non-generator endpoints#16192
SEPURI-SAI-KRISHNA wants to merge 1 commit into
fastapi:masterfrom
SEPURI-SAI-KRISHNA:fix-sse-non-generator-dispatch

Conversation

@SEPURI-SAI-KRISHNA

@SEPURI-SAI-KRISHNA SEPURI-SAI-KRISHNA commented Aug 14, 2026

Copy link
Copy Markdown

See #16107

A path operation declared with response_class=EventSourceResponse that is not a generator is dispatched into the SSE branch anyway, because get_request_handler() recomputes is_sse_stream from the response class alone, with no generator check.

@app.get("/events", response_class=EventSourceResponse)
async def events() -> EventSourceResponse:
    return EventSourceResponse(gen())
# returns 200 text/event-stream with an empty body

The branch is entered before the endpoint runs, so dependant.call(...) yields a coroutine (or a plain value) where an iterator is expected, and the 200 and text/event-stream headers are already flushed by the time it fails. Two symptoms, both silent:

  • async def returns 200 with an empty body, plus RuntimeWarning: coroutine ... was never awaited. Under uvicorn it is worse than empty: iterate_in_threadpool() raises TypeError: 'coroutine' object is not iterable mid-response, the connection truncates, and the client gets RemoteProtocolError: peer closed connection without sending complete message body.
  • sync def returns 200 streaming the iteration of the return value with no error at all, so an endpoint returning {"msg": "hello"} puts data: "msg" on the wire.

The same value is already computed correctly on the route, with the generator check fused in, right beside its JSONL sibling:

route.is_sse_stream = is_generator and lenient_issubclass(
    response_class, EventSourceResponse
)
route.is_json_stream = is_generator and isinstance(response_class, DefaultPlaceholder)

but only is_json_stream is passed down, so the two disagree for every non-generator endpoint.

This PR removes the local recomputation and threads route.is_sse_stream through as a parameter, exactly as route.is_json_stream already was, so both flags are derived in one place. The local recomputation appears to have guarded against response_class being a DefaultPlaceholder, but add_api_route() already resolves that via get_value_or_default() before the route is built, so the route attribute is correct in precisely that case — verified for app-level and router-level default_response_class=EventSourceResponse, where route.is_sse_stream stays True and OpenAPI still documents text/event-stream.

Present since SSE was added in 0.135.0; verified against stock 0.135.0 and 0.141.0.

Adds tests/test_sse_non_generator.py covering:

  • a non-generator returning an EventSourceResponse directly
  • an async non-generator returning a plain value
  • a sync non-generator returning a plain value (the silent-corruption case)
  • a generator, as the existing control case for normal SSE streaming

Three of the four fail on master — two with TypeError: 'coroutine' object is not iterable, and the sync one with a plain assertion showing data: "msg" on the wire.

@SEPURI-SAI-KRISHNA SEPURI-SAI-KRISHNA changed the title 🐛 Fix SSE dispatch selecting the streaming branch for non-generator endpoints 🐛 Fix response_class=EventSourceResponse sending an empty body for non-generator endpoints Aug 14, 2026
@codspeed-hq

codspeed-hq Bot commented Aug 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing SEPURI-SAI-KRISHNA:fix-sse-non-generator-dispatch (11d55a4) with master (cdbb660)1

Open in CodSpeed

Footnotes

  1. No successful run was found on master (f336ff8) during the generation of this report, so cdbb660 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant