Skip to content

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 options response_model_exclude_unset=, response_model_exclude_none=, and response_model_include= / response_model_exclude= as a literal set or list of the model's field names.
  • A Response class as the return annotation (-> RedirectResponse, -> Response) is no response model, like FastAPI. FastAPI(default_response_class=HTMLResponse) (or PlainTextResponse, JSONResponse).
  • Generator endpoints (FastAPI ≥ 0.134, an endpoint that yields): JSON Lines by default (application/jsonl, each item validated with the item type of an AsyncIterable[Item]/Iterable[Item] return annotation, else json.dumps(jsonable_encoder(item))), Server-Sent Events with response_class=EventSourceResponse (text/event-stream, ServerSentEvent(data=, raw_data=, event=, id=, retry=, comment=) or plain items as data:, a : ping comment after 15 s without an item), the items as they are with response_class=StreamingResponse. An exception in the generator cuts the connection, as uvicorn does once the response has started.
  • Returned Response, JSONResponse, PlainTextResponse, HTMLResponse, RedirectResponse, FileResponse are sent like Starlette 1.7 (header order, ETag/Last-Modified of FileResponse; no Range/partial HEAD). JSONResponse(content=...) serializes like Starlette (strict json.dumps: a model, a datetime or NaN raise). set_cookie/delete_cookie in http.cookies format, headers set on the injected Response.
  • response_class= on a route (HTMLResponse, PlainTextResponse, Response, RedirectResponse, FileResponse, JSONResponse): a returned non-response value is encoded (response_model or jsonable_encoder), then wrapped like FastAPI (str/bytes/None body, a returned URL redirects with 307 unless status_code= is given, a returned path is served as a file, empty body for 204/304, the injected Response's headers appended). response_class=StreamingResponse with a non-response return value raises (500).
  • jsonable_encoder semantics for returned values (models by alias, bytes.decode(), Decimal via FastAPI's decimal_encoder).
  • Large lists returned straight from the session are streamed: see Large list responses.
  • StreamingResponse and async generators (SSE). await request.is_disconnected() always returns False: a disconnected client is noticed at the next send, then the generator's finally runs.
  • 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 (a bytes, a numeric string for retry) raise a TypeError instead.
  • Other response_class= classes (ORJSONResponse, UJSONResponse, a project subclass) are refused at transpile time.

Differences

  • Known difference: a mapped object returned without response_model is 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).