JSONL streaming responses miss the anti-buffering headers (Cache-Control, X-Accel-Buffering) that SSE sets #15794
Replies: 8 comments 1 reply
-
|
Confirmed on 0.137.x — the asymmetry is real. In The JSONL branch ( We hit this class of issue in production with SSE behind Nginx: without On your open question: I'd include both headers, same as SSE. A 3-line mirror + a |
Beta Was this translation helpful? Give feedback.
-
|
Thanks @fournarin, that's really useful — especially the production detail about the Agreed on including both headers to match SSE. The silent regression when someone switches I've got the fix ready locally: the 3-line mirror in the Happy to open a PR whenever a maintainer is good with the approach. 🙏 |
Beta Was this translation helpful? Give feedback.
-
|
Appreciate it @fournarin — and genuinely, thanks for taking the time to reproduce it on 0.137.x and to bring in the production angle. The detail about the On the "mark as answer" request — I'm going to hold off on that for now, and I hope it doesn't come across the wrong way. The reason is just that this isn't really a Q&A thread where a comment is the resolution: it's a bug report with a proposed fix, and what it's actually waiting on is a maintainer being okay with the approach so the PR can go in. If I mark it answered now, it drops out of the unanswered queue that the team and the FastAPI Experts triage from, and I'd rather keep it visible until the fix is actually merged. Once the PR lands I'm happy to come back, link it here, and wrap the thread up properly. None of that takes away from your comment being useful — it settled my open question (include both headers, matching SSE) and gave me a concrete real-world failure mode to point at, which is more than enough to move forward with confidence. For what it's worth, the fix is small and ready on my end: the three-line mirror in the Thanks again for the careful look — and if a maintainer does swing by, your production confirmation here will probably help make the case. 🙏 |
Beta Was this translation helpful? Give feedback.
-
|
Until the headers are added to the built-in JSONL branch, you can get the same proxy-safe behavior today by returning a from collections.abc import AsyncIterable
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
async def item_stream() -> AsyncIterable[str]:
for item in (Item(name="a"), Item(name="b")):
yield item.model_dump_json() + "\n"
@app.get("/jsonl-items")
async def jsonl_items():
return StreamingResponse(
item_stream(),
media_type="application/x-ndjson",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no", # stop Nginx (proxy_buffering on) from buffering
},
)On the proposal itself: mirroring the SSE branch makes sense — both are incremental streams, so both should send |
Beta Was this translation helpful? Give feedback.
-
|
Thanks @delcenjo — that's a solid stopgap, and worth having in the thread for anyone hitting this before the fix lands. Dropping down to a manual And good to have another confirmation that mirroring the SSE branch is the right call. The fix is ready on my end (the 3-line mirror plus a header-asserting test), so it's a quick PR whenever a maintainer gives the go-ahead. 🙏 |
Beta Was this translation helpful? Give feedback.
-
|
Opened the PR: #15813. It mirrors the SSE branch — sets |
Beta Was this translation helpful? Give feedback.
-
|
Hello @torrresagus as another workaround you can use this approach for your example. from collections.abc import AsyncIterable
from fastapi import FastAPI, Depends
from fastapi.sse import EventSourceResponse
from fastapi.responses import Response
from fastapi.testclient import TestClient
from pydantic import BaseModel
class Item(BaseModel):
name: str
app = FastAPI()
async def live_jsonl_headers(response:Response) -> Response:
response.headers["Cache-Control"] = "no-cache"
response.headers["X-Accel-Buffering"] = "no"
return response
@app.get("/sse-items", response_class=EventSourceResponse)
async def sse_items() -> AsyncIterable[Item]:
# text/event-stream response (SSE)
yield Item(name="a")
@app.get("/jsonl-items")
async def jsonl_items(response: Response = Depends(live_jsonl_headers)) -> AsyncIterable[Item]:
# application/jsonl response (JSON Lines streaming)
yield Item(name="a")
client = TestClient(app)
print("SSE ->", dict(client.get("/sse-items").headers))
# SSE -> {'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache', 'x-accel-buffering': 'no'}
print("JSONL->", dict(client.get("/jsonl-items").headers))
# JSONL-> {'content-type': 'application/jsonl', 'cache-control': 'no-cache', 'x-accel-buffering': 'no' |
Beta Was this translation helpful? Give feedback.
-
|
Quick update for anyone following this: after review feedback from @luzzodev on the PR (#15813), the approach changed slightly. JSON Lines isn't only a live event stream like SSE — it's also used for bulk/streamed exports where caching can be legitimate. Since The PR now sets only Thanks @fournarin and @delcenjo for the earlier confirmation and production context, and @luzzodev for the review. 🙏 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
First Check
Commit to Help
Example Code
Description
This is a small bug report with a proposed fix (happy to open the PR).
FastAPI's Server-Sent Events responses set two headers so streaming survives a proxy (
fastapi/routing.py, theis_sse_streambranch, added in #15030):The JSON Lines streaming responses (the
yield-basedapplication/jsonlstreaming added in #15022, theis_json_streambranch) stream items incrementally in the same way, but do not set those headers.proxy_buffering on), the JSONL lines are buffered and flushed together, which defeats the streaming. SSE is unaffected because it already sendsX-Accel-Buffering: no.The two features landed in separate PRs (SSE #15030, JSONL #15022), which is likely why only the SSE path got the headers.
Proposed fix: set the same
Cache-Control: no-cacheandX-Accel-Buffering: noon the JSONLStreamingResponse, mirroring the SSE branch (a 3-line change), plus a test asserting the headers. I have this implemented locally and the existing SSE + streaming test suites still pass.Would this be welcome as a PR? One open question: include
Cache-Control: no-cachetoo (for full consistency with SSE), or onlyX-Accel-Buffering: no?Operating System
Linux
Operating System Details
WSL2 (Linux 6.6)
FastAPI Version
0.137.2
Pydantic Version
2.13.4
Python Version
3.13.7
Additional Context
No response
Beta Was this translation helpful? Give feedback.
All reactions