Browse Source

🐛 Drop Cache-Control from JSONL streaming, keep only X-Accel-Buffering, document behavior

Per review feedback: JSON Lines is not only a live event stream like SSE,
it's also used for bulk/streamed exports where caching can be legitimate.
`Cache-Control: no-cache` forces revalidation and would take away the
`max-age` "serve from cache" option from those endpoints, so it shouldn't
be imposed by default.

Keep only `X-Accel-Buffering: no` (buffering defeats incremental streaming
in every case) and leave caching headers to the user. Document the proxy
buffering behavior in the JSON Lines tutorial.
pull/15813/head
Agustin Torres 4 weeks ago
parent
commit
ac2138551e
  1. 12
      docs/en/docs/tutorial/stream-json-lines.md
  2. 5
      fastapi/routing.py
  3. 9
      tests/test_tutorial/test_stream_json_lines/test_tutorial001.py

12
docs/en/docs/tutorial/stream-json-lines.md

@ -72,6 +72,18 @@ If you want to stream binary data, for example video or audio, check the advance
///
## Proxy Buffering { #proxy-buffering }
When a JSON Lines response is served behind a proxy that buffers responses by default (for example Nginx with `proxy_buffering on`), the proxy could hold back the lines and deliver them all at once, which would defeat the streaming.
To prevent that, FastAPI sets the `X-Accel-Buffering: no` header on JSON Lines responses, telling Nginx (and compatible proxies) not to buffer them.
/// note
Caching headers (like `Cache-Control`) are **not** set automatically, they are left up to you. JSON Lines is also used for bulk exports where caching can be legitimate, so FastAPI doesn't impose a caching policy. If you need one, you can set it on the [`Response`](../advanced/response-headers.md).
///
## Stream JSON Lines with FastAPI { #stream-json-lines-with-fastapi }
To stream JSON Lines with FastAPI you can, instead of using `return` in your *path operation function*, use `yield` to produce each item in turn.

5
fastapi/routing.py

@ -658,8 +658,9 @@ def get_request_handler(
media_type="application/jsonl",
background=solved_result.background_tasks,
)
response.headers["Cache-Control"] = "no-cache"
# For Nginx proxies to not buffer the streamed response
# For Nginx proxies to not buffer the streamed response.
# Caching headers are intentionally left to the user, as JSONL
# is also used for bulk exports where caching can be legitimate.
response.headers["X-Accel-Buffering"] = "no"
response.headers.raw.extend(solved_result.response.headers.raw)
elif dependant.is_async_gen_callable or dependant.is_gen_callable:

9
tests/test_tutorial/test_stream_json_lines/test_tutorial001.py

@ -52,14 +52,13 @@ def test_stream_items(client: TestClient, path: str):
"/items/stream-no-async-no-annotation",
],
)
def test_stream_sets_streaming_headers(client: TestClient, path: str):
"""JSONL streaming responses should disable proxy buffering and caching,
so intermediaries deliver items incrementally (matching SSE responses)."""
def test_stream_disables_proxy_buffering(client: TestClient, path: str):
"""JSONL streaming responses should disable proxy buffering, so
intermediaries (e.g. Nginx) deliver items incrementally instead of
holding them back until the buffer fills."""
response = client.get(path)
assert response.status_code == 200, response.text
# Tells proxies (e.g. Nginx) not to buffer the streamed response
assert response.headers["x-accel-buffering"] == "no"
assert response.headers["cache-control"] == "no-cache"
def test_openapi_schema(client: TestClient):

Loading…
Cancel
Save