Responses¶
FastAPI turns what an endpoint returns into a response: filtered through response_model, encoded by
jsonable_encoder, or sent as is when it is a Response object. The binary reproduces those rules and
Starlette's response classes, header order included.
A response_model that filters a field out, status codes, response classes (plain text, HTML,
redirect, JSON with a custom status), a cookie and a header set on the injected Response, and a server-sent
events stream. Like every example on this site, it is compiled and compared with FastAPI in CI (how).
docs_src/tutorial/responses.py
from fastapi import APIRouter, Response, status
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse, StreamingResponse
from pydantic import BaseModel
router = APIRouter(prefix="/responses", tags=["responses"])
class UserIn(BaseModel):
username: str
password: str
email: str | None = None
class UserOut(BaseModel):
username: str
email: str | None = None
@router.post("/users", response_model=UserOut, status_code=status.HTTP_201_CREATED)
async def create_user(user: UserIn):
return user # the response model leaves the password out
class Item(BaseModel):
name: str
description: str | None = None
price: float
tags: list[str] = []
ITEMS = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tags": []},
}
@router.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
return ITEMS[item_id]
@router.get("/hello", response_class=PlainTextResponse)
async def hello():
return "Hello, world"
@router.get("/page", response_class=HTMLResponse)
async def page():
return "<h1>Compiled by py2axum</h1>"
@router.get("/old-hello")
async def old_hello():
return RedirectResponse("/responses/hello")
@router.get("/cookie")
async def set_cookie(response: Response):
response.set_cookie("session", "abc123", httponly=True, max_age=3600)
response.headers["X-Cache"] = "miss"
return {"ok": True}
@router.get("/teapot")
async def teapot():
return JSONResponse({"detail": "I'm a teapot"}, status_code=418)
async def countdown(n: int):
for i in range(n, 0, -1):
yield f"data: {i}\n\n"
yield "data: liftoff\n\n"
@router.get("/countdown")
async def events(n: int = 3):
return StreamingResponse(countdown(n), media_type="text/event-stream")
What is native¶
response_model(filtering, aliases), status codes, returned dicts, lists, models, ORM objects (from_attributes). The route optionsresponse_model_exclude_unset=,response_model_exclude_none=, andresponse_model_include=/response_model_exclude=as a literal set or list of the model's field names.- A
Responseclass as the return annotation (-> RedirectResponse,-> Response) is no response model, like FastAPI.FastAPI(default_response_class=HTMLResponse)(orPlainTextResponse,JSONResponse). - Generator endpoints (FastAPI ≥ 0.134, an endpoint that
yields): JSON Lines by default (application/jsonl, each item validated with the item type of anAsyncIterable[Item]/Iterable[Item]return annotation, elsejson.dumps(jsonable_encoder(item))), Server-Sent Events withresponse_class=EventSourceResponse(text/event-stream,ServerSentEvent(data=, raw_data=, event=, id=, retry=, comment=)or plain items asdata:, a: pingcomment after 15 s without an item), the items as they are withresponse_class=StreamingResponse. An exception in the generator cuts the connection, as uvicorn does once the response has started. - Returned
Response,JSONResponse,PlainTextResponse,HTMLResponse,RedirectResponse,FileResponseare sent like Starlette 1.7 (header order,ETag/Last-ModifiedofFileResponse; noRange/partialHEAD).JSONResponse(content=...)serializes like Starlette (strictjson.dumps: a model, a datetime or NaN raise).set_cookie/delete_cookieinhttp.cookiesformat, headers set on the injectedResponse. response_class=on a route (HTMLResponse,PlainTextResponse,Response,RedirectResponse,FileResponse,JSONResponse): a returned non-response value is encoded (response_modelorjsonable_encoder), then wrapped like FastAPI (str/bytes/Nonebody, a returned URL redirects with 307 unlessstatus_code=is given, a returned path is served as a file, empty body for 204/304, the injectedResponse's headers appended).response_class=StreamingResponsewith a non-response return value raises (500).jsonable_encodersemantics for returned values (models by alias,bytes.decode(),Decimalvia FastAPI'sdecimal_encoder).- Large lists returned straight from the session are streamed: see Large list responses.
StreamingResponseand async generators (SSE).await request.is_disconnected()always returnsFalse: a disconnected client is noticed at the next send, then the generator'sfinallyruns.- Response status codes: uvicorn has a status line for 100..599 only and drops the connection, unanswered, for
any other
status_code; the binary does the same.
Errors (HTTPException, exception handlers) are in Handling errors; templates
(Jinja2Templates.TemplateResponse) in Libraries.
What stays in Python¶
response_model_include=/_exclude=with a nested selection (a dict), a computed field, or on another response model than a Pydantic model;response_model_exclude_defaults=,response_model_by_alias=.- A generator endpoint with another response class, or with
response_model_*options.ServerSentEvent(...)values pydantic would coerce (abytes, a numeric string forretry) raise aTypeErrorinstead. - Other
response_class=classes (ORJSONResponse,UJSONResponse, a project subclass) are refused at transpile time.
Differences¶
- Known difference: a mapped object returned without
response_modelis encoded like FastAPI does (vars(obj)without SQLAlchemy's_sa_*keys: its loaded attributes), but its keys come in column order. CPython's order follows SQLAlchemy's iteration over a set of columns, which depends on memory addresses: it changes from one process to the next, so no order can be reproduced. Same keys and values. - Known difference: a final 1xx status is a 500 (hyper does not send an informational status as the final response).