diff --git a/docs/en/docs/tutorial/stream-json-lines.md b/docs/en/docs/tutorial/stream-json-lines.md index ea1b781a7..d7bfa1ac4 100644 --- a/docs/en/docs/tutorial/stream-json-lines.md +++ b/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. diff --git a/fastapi/routing.py b/fastapi/routing.py index 78207778a..92b77e57f 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -666,6 +666,10 @@ def get_request_handler( media_type="application/jsonl", background=solved_result.background_tasks, ) + # 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: # Raw streaming with explicit response_class (e.g. StreamingResponse) diff --git a/tests/test_tutorial/test_stream_json_lines/test_tutorial001.py b/tests/test_tutorial/test_stream_json_lines/test_tutorial001.py index 0b5f9d95b..dec82173e 100644 --- a/tests/test_tutorial/test_stream_json_lines/test_tutorial001.py +++ b/tests/test_tutorial/test_stream_json_lines/test_tutorial001.py @@ -43,6 +43,24 @@ def test_stream_items(client: TestClient, path: str): assert lines == expected_items +@pytest.mark.parametrize( + "path", + [ + "/items/stream", + "/items/stream-no-async", + "/items/stream-no-annotation", + "/items/stream-no-async-no-annotation", + ], +) +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 + assert response.headers["x-accel-buffering"] == "no" + + def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text