Should EventSourceResponse return standard HTTP errors for exceptions raised before the stream starts?
#15129
Replies: 5 comments
-
|
Hi there! Thanks for detailed write-up. This is a very common scenario when dealing with streaming responses in Python and ASGI frameworks. The issue you're experiencing with the "generator trap" (where pre-stream exceptions aren't raised until iteration begins) is tied to how Python generators work. Fortunately, your second approach (separating the pre-stream logic from the generator) is exactly the right mental model! The reason it raised a When you return a Response object (like Here is the corrected, idiomatic way to achieve exactly what you want: clean HTTP exceptions before the stream, and standard SSE behavior after. from fastapi import FastAPI, HTTPException
# Assuming you are using sse_starlette (FastAPI doesn't have fastapi.sse built-in)
from sse_starlette.sse import EventSourceResponse, ServerSentEvent
app = FastAPI()
# 1. REMOVE response_class=EventSourceResponse from the decorator
@app.get("/hello")
async def hello_world():
# 2. Pre-stream logic runs synchronously here.
# This will correctly return a standard JSON HTTP 501 response!
raise HTTPException(status_code=501, detail="This endpoint is not yet implemented")
# 3. Define the async generator for the stream
async def _hello_world_generator():
for char in "Hello, World!":
yield ServerSentEvent(data=char)
# 4. Return the instantiated Response object directly
return EventSourceResponse(_hello_world_generator())Why this solves your problem: Because this pattern fully handles the separation of "normal request errors" vs "active stream errors" without requiring modifications to FastAPI's core routing logic, a change to Hope this helps! Let me know if you run into any other issues. |
Beta Was this translation helpful? Give feedback.
-
|
I found a potential workaround by moving the pre-stream logic into FastAPI dependencies, since dependencies are resolved before the generator starts being consumed. This allows validation and similar checks to raise That said, it still seems worth discussing whether this behavior should be handled more explicitly at the framework level. |
Beta Was this translation helpful? Give feedback.
-
|
Quick correction first: Why pre-stream exceptions are silently lostWhen your endpoint is an async generator ( # BAD — raise before yield never fires at request time
@app.get("/hello", response_class=EventSourceResponse)
async def hello_world() -> AsyncIterable[ServerSentEvent]:
raise HTTPException(status_code=501) # ← runs during iteration, not on request
for char in "Hello, World!":
yield ServerSentEvent(data=char)Solution 1 — FastAPI Dependency (recommended)Dependencies are resolved synchronously before the generator is created, so from collections.abc import AsyncIterable
from fastapi import Depends, FastAPI, HTTPException
from fastapi.sse import EventSourceResponse, ServerSentEvent
app = FastAPI()
async def check_access():
"""Raise HTTPException here to block the stream before it starts."""
authorized = False # your real check here
if not authorized:
raise HTTPException(status_code=403, detail="Forbidden")
@app.get("/hello", response_class=EventSourceResponse)
async def hello_world(
_: None = Depends(check_access),
) -> AsyncIterable[ServerSentEvent]:
for char in "Hello, World!":
yield ServerSentEvent(data=char)A Solution 2 — Separate pre-stream logic from the generatorIf your pre-stream logic needs the same scope as the generator (e.g. a DB connection), keep them in the same function but separate the non-generator part: @app.get("/hello")
async def hello_world():
# Everything here runs synchronously on the request
authorized = False # your real check
if not authorized:
raise HTTPException(status_code=403, detail="Forbidden")
async def _stream() -> AsyncIterable[ServerSentEvent]:
for char in "Hello, World!":
yield ServerSentEvent(data=char)
# Return the EventSourceResponse directly (no response_class on the decorator)
return EventSourceResponse(_stream())Note: when you return TL;DR: use a |
Beta Was this translation helpful? Give feedback.
This comment was marked as spam.
This comment was marked as spam.
-
|
I also have a setup where I directly return an |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
First Check
Commit to Help
Example Code
Also tried this but it raises
TypeError: 'coroutine' object is not iterablewhich is explained by this.Description
When using
EventSourceResponsewith FastAPI, the endpoint function effectively becomes a generator-based streaming endpoint.Reference:
Problem
In production systems, streaming endpoints frequently require pre-stream logic.
These steps may legitimately raise
HTTPExceptionor other exceptions that are mapped to exception handlers.Desired behavior
If an exception occurs before the streaming response begins, it would be preferable for FastAPI to return a standard HTTP error response (e.g.,
JSONResponsefrom the defaultHTTPExceptionhandler).This maintains a clean separation between:
Rationale
From an API design perspective, errors that occur prior to emitting the first stream chunk are still part of the normal request lifecycle and should follow the usual HTTP error semantics.
Once the stream begins (headers sent + body streaming), error handling must follow streaming semantics (SSE error events).
Separating these two phases helps avoid ambiguous responses and aligns better with how HTTP streaming works.
Proposal
This is a rough idea and may not be the correct implementation approach.
Introduce an additional check around https://github.com/fastapi/fastapi/blob/0.135.1/fastapi/routing.py#L525-L528.
If the resolved response class is
EventSourceResponse, the endpoint function could first be executed as a normal coroutine. Any exceptions raised during this phase would propagate through FastAPI’s standard exception handling flow.If execution succeeds, the returned async generator could then be extracted, after which the SSE stream would proceed as usual.
Operating System
macOS
Operating System Details
No response
FastAPI Version
0.135.1
Pydantic Version
2.12.5
Python Version
3.12.10
Additional Context
No response
Beta Was this translation helpful? Give feedback.
All reactions