Browse Source

📝 Document stream response headers

pull/15953/head
Manjari Yagnik 2 weeks ago
parent
commit
a9b2d8721c
Failed to extract signature
  1. 12
      docs/en/docs/tutorial/server-sent-events.md
  2. 12
      docs/en/docs/tutorial/stream-json-lines.md
  3. 33
      docs_src/server_sent_events/tutorial006_py310.py
  4. 28
      docs_src/stream_json_lines/tutorial002_py310.py
  5. 96
      tests/test_tutorial/test_server_sent_events/test_tutorial006.py
  6. 82
      tests/test_tutorial/test_stream_json_lines/test_tutorial002.py

12
docs/en/docs/tutorial/server-sent-events.md

@ -69,6 +69,18 @@ You can also omit the return type. FastAPI will use the [`jsonable_encoder`](./e
{* ../../docs_src/server_sent_events/tutorial001_py310.py ln[34:37] hl[35] *}
## Custom Response Headers { #custom-response-headers }
For normal responses, you can set custom headers using a `Response` parameter, as explained in [Response Headers](../advanced/response-headers.md#use-a-response-parameter).
With an SSE stream, the *path operation function* is a generator. Calling it creates the generator, but the code inside the generator body doesn't run until the response is already streaming. So setting headers inside the generator body is too late for headers that need to be sent with the response.
Instead, use a dependency that receives the `Response` parameter and sets the headers before the stream starts:
{* ../../docs_src/server_sent_events/tutorial006_py310.py ln[1:33] hl[3:4,22:23,26:30] *}
Dependencies run before FastAPI creates the streaming response, so FastAPI can copy those headers into the final SSE response.
## `ServerSentEvent` { #serversentevent }
If you need to set SSE fields like `event`, `id`, `retry`, or `comment`, you can yield `ServerSentEvent` objects instead of plain data.

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

@ -106,6 +106,18 @@ You can also omit the return type. FastAPI will then use the [`jsonable_encoder`
{* ../../docs_src/stream_json_lines/tutorial001_py310.py ln[33:36] hl[34] *}
## Custom Response Headers { #custom-response-headers }
For normal responses, you can set custom headers using a `Response` parameter, as explained in [Response Headers](../advanced/response-headers.md#use-a-response-parameter).
With a JSON Lines stream, the *path operation function* is a generator. Calling it creates the generator, but the code inside the generator body doesn't run until the response is already streaming. So setting headers inside the generator body is too late for headers that need to be sent with the response.
Instead, use a dependency that receives the `Response` parameter and sets the headers before the stream starts:
{* ../../docs_src/stream_json_lines/tutorial002_py310.py ln[1:28] hl[3,21:22,25] *}
Dependencies run before FastAPI creates the streaming response, so FastAPI can copy those headers into the final JSON Lines response.
## Server-Sent Events (SSE) { #server-sent-events-sse }
FastAPI also has first-class support for Server-Sent Events (SSE), which are quite similar but with a couple of extra details. You can learn about them in the next chapter: [Server-Sent Events (SSE)](server-sent-events.md). 🤓

33
docs_src/server_sent_events/tutorial006_py310.py

@ -0,0 +1,33 @@
from collections.abc import AsyncIterable
from fastapi import Depends, FastAPI, Response
from fastapi.sse import EventSourceResponse
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None
items = [
Item(name="Plumbus", description="A multi-purpose household device."),
Item(name="Portal Gun", description="A portal opening device."),
Item(name="Meeseeks Box", description="A box that summons a Meeseeks."),
]
async def add_stream_headers(response: Response) -> None:
response.headers["X-Stream-Source"] = "inventory"
@app.get(
"/items/stream",
response_class=EventSourceResponse,
dependencies=[Depends(add_stream_headers)],
)
async def sse_items() -> AsyncIterable[Item]:
for item in items:
yield item

28
docs_src/stream_json_lines/tutorial002_py310.py

@ -0,0 +1,28 @@
from collections.abc import AsyncIterable
from fastapi import Depends, FastAPI, Response
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None
items = [
Item(name="Plumbus", description="A multi-purpose household device."),
Item(name="Portal Gun", description="A portal opening device."),
Item(name="Meeseeks Box", description="A box that summons a Meeseeks."),
]
async def add_stream_headers(response: Response) -> None:
response.headers["X-Stream-Source"] = "inventory"
@app.get("/items/stream", dependencies=[Depends(add_stream_headers)])
async def stream_items() -> AsyncIterable[Item]:
for item in items:
yield item

96
tests/test_tutorial/test_server_sent_events/test_tutorial006.py

@ -0,0 +1,96 @@
import importlib
import pytest
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
@pytest.fixture(
name="client",
params=[
pytest.param("tutorial006_py310"),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.server_sent_events.{request.param}")
client = TestClient(mod.app)
return client
def test_stream_items_with_custom_header(client: TestClient):
response = client.get("/items/stream")
assert response.status_code == 200, response.text
assert response.headers["content-type"] == "text/event-stream; charset=utf-8"
assert response.headers["x-stream-source"] == "inventory"
data_lines = [
line for line in response.text.strip().split("\n") if line.startswith("data: ")
]
assert len(data_lines) == 3
assert '"name":"Plumbus"' in data_lines[0] or '"name": "Plumbus"' in data_lines[0]
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/stream": {
"get": {
"summary": "Sse Items",
"operationId": "sse_items_items_stream_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"text/event-stream": {
"itemSchema": {
"type": "object",
"properties": {
"data": {
"type": "string",
"contentMediaType": "application/json",
"contentSchema": {
"$ref": "#/components/schemas/Item"
},
},
"event": {"type": "string"},
"id": {"type": "string"},
"retry": {
"type": "integer",
"minimum": 0,
},
},
"required": ["data"],
}
}
},
}
},
}
}
},
"components": {
"schemas": {
"Item": {
"properties": {
"name": {"type": "string", "title": "Name"},
"description": {
"anyOf": [
{"type": "string"},
{"type": "null"},
],
"title": "Description",
},
},
"type": "object",
"required": ["name", "description"],
"title": "Item",
}
}
},
}
)

82
tests/test_tutorial/test_stream_json_lines/test_tutorial002.py

@ -0,0 +1,82 @@
import importlib
import json
import pytest
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
@pytest.fixture(
name="client",
params=[
pytest.param("tutorial002_py310"),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.stream_json_lines.{request.param}")
client = TestClient(mod.app)
return client
def test_stream_items_with_custom_header(client: TestClient):
response = client.get("/items/stream")
assert response.status_code == 200, response.text
assert response.headers["content-type"] == "application/jsonl"
assert response.headers["x-stream-source"] == "inventory"
lines = [json.loads(line) for line in response.text.strip().splitlines()]
assert lines == [
{"name": "Plumbus", "description": "A multi-purpose household device."},
{"name": "Portal Gun", "description": "A portal opening device."},
{"name": "Meeseeks Box", "description": "A box that summons a Meeseeks."},
]
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/stream": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/jsonl": {
"itemSchema": {
"$ref": "#/components/schemas/Item"
},
}
},
}
},
"summary": "Stream Items",
"operationId": "stream_items_items_stream_get",
}
}
},
"components": {
"schemas": {
"Item": {
"properties": {
"name": {"type": "string", "title": "Name"},
"description": {
"anyOf": [
{"type": "string"},
{"type": "null"},
],
"title": "Description",
},
},
"type": "object",
"required": ["name", "description"],
"title": "Item",
}
}
},
}
)
Loading…
Cancel
Save