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 } ## 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. 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", media_type="application/jsonl",
background=solved_result.background_tasks, 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["X-Accel-Buffering"] = "no"
response.headers.raw.extend(solved_result.response.headers.raw) response.headers.raw.extend(solved_result.response.headers.raw)
elif dependant.is_async_gen_callable or dependant.is_gen_callable: 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", "/items/stream-no-async-no-annotation",
], ],
) )
def test_stream_sets_streaming_headers(client: TestClient, path: str): def test_stream_disables_proxy_buffering(client: TestClient, path: str):
"""JSONL streaming responses should disable proxy buffering and caching, """JSONL streaming responses should disable proxy buffering, so
so intermediaries deliver items incrementally (matching SSE responses).""" intermediaries (e.g. Nginx) deliver items incrementally instead of
holding them back until the buffer fills."""
response = client.get(path) response = client.get(path)
assert response.status_code == 200, response.text 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["x-accel-buffering"] == "no"
assert response.headers["cache-control"] == "no-cache"
def test_openapi_schema(client: TestClient): def test_openapi_schema(client: TestClient):

Loading…
Cancel
Save