Responses¶
This guide covers all the response types available in Django-Bolt and how to use them.
JSON responses¶
Returning a dict, list, or msgspec.Struct automatically creates a JSON response:
@api.get("/data")
async def get_data():
return {"message": "Hello", "count": 42}
@api.get("/items")
async def get_items():
return [{"id": 1}, {"id": 2}]
Custom status codes and headers¶
Use the JSON class for more control:
from django_bolt import JSON
@api.post("/users")
async def create_user():
return JSON(
{"id": 1, "username": "john"},
status_code=201,
headers={"X-Created-By": "django-bolt"}
)
Status code constants and helpers¶
You can use built-in status code constants for readability:
from django_bolt import status
@api.post("/users", status_code=status.HTTP_201_CREATED)
async def create_user():
...
Code classification helpers are also available:
if status.is_client_error(response.status_code):
...
if status.is_server_error(response.status_code):
...
Plain text¶
Return plain text responses:
from django_bolt.responses import PlainText
@api.get("/hello")
async def hello():
return PlainText("Hello, World!")
@api.get("/status")
async def status():
return PlainText("OK", status_code=200, headers={"X-Status": "healthy"})
HTML¶
Return HTML content:
from django_bolt.responses import HTML
@api.get("/page")
async def page():
return HTML("<h1>Welcome</h1><p>This is HTML content.</p>")
@api.get("/template")
async def template():
html = """
<!DOCTYPE html>
<html>
<head><title>My Page</title></head>
<body><h1>Hello</h1></body>
</html>
"""
return HTML(html)
Django templates¶
Use the render() function to render Django templates. It works like Django's render() shortcut:
from django_bolt import Request
from django_bolt.shortcuts import render
@api.get("/page")
async def show_page(request: Request):
return render(request, "myapp/page.html", {
"title": "My Page",
"items": ["item1", "item2"],
})
The auth context processor gives the template the user of the request. In an async handler, a template that reads {{ user }} loads the user, and the event loop waits for the query. To not block the loop, load the user first:
@api.get("/account")
async def account(request: Request):
await request.auser() # the template then reads the loaded user
return render(request, "myapp/account.html")
Use standard Django templates - nothing special required:
<!-- templates/myapp/page.html -->
<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
<h1>{{ title }}</h1>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</body>
</html>
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
request |
Request |
required | The request object |
template_name |
str |
required | Path to the template file |
context |
dict |
None |
Template context variables |
content_type |
str |
None |
Response content type |
status |
int |
200 |
HTTP status code |
using |
str |
None |
Template engine to use |
Redirects¶
Redirect to another URL:
from django_bolt.responses import Redirect
@api.get("/old-page")
async def old_page():
return Redirect("/new-page")
@api.get("/external")
async def external():
return Redirect("https://example.com", status_code=302)
Redirect status codes:
301- Permanent redirect302- Temporary redirect (Found)303- See Other307- Temporary redirect (default, preserves method)308- Permanent redirect (preserves method)
File downloads¶
In-memory files¶
Use File for small files that can be loaded into memory:
from django_bolt.responses import File
@api.get("/download")
async def download():
return File(
"/path/to/file.pdf",
filename="document.pdf",
media_type="application/pdf"
)
Streaming files¶
Use FileResponse for larger files that should be streamed:
from django_bolt.responses import FileResponse
@api.get("/video")
async def video():
return FileResponse(
"/path/to/video.mp4",
filename="video.mp4",
media_type="video/mp4"
)
FileResponse streams the file directly without loading it entirely into memory.
File security¶
Configure allowed directories in settings.py:
When configured, FileResponse only serves files within these directories, preventing path traversal attacks.
A FileResponse for a path outside these directories raises ResponseFileNotAllowed, and Bolt answers 403, also when the path names no file. When the lookup fails, Bolt follows the links of the path, as realpath does, to where the lookup stops. Then it completes the path from there as text. A FileResponse for a path that names no regular file raises ResponseFileNotFound, and Bolt answers 404. Examples are a missing file, a directory and a path that is too long. The path often comes from the request, so a client does not get a 500 for it. The errors subclass PermissionError and FileNotFoundError. The 404 and 403 responses do not show the path. Bolt answers 500 for a FileNotFoundError or a PermissionError of other code, because it is a server fault.
Streaming responses¶
Stream data incrementally using generators:
from django_bolt import StreamingResponse
@api.get("/stream")
async def stream():
def generate():
for i in range(100):
yield f"chunk {i}\n"
return StreamingResponse(generate(), media_type="text/plain")
Async generators¶
For async operations, use async generators:
import asyncio
@api.get("/async-stream")
async def async_stream():
async def generate():
for i in range(10):
await asyncio.sleep(0.1)
yield f"data: {i}\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
Errors in a stream¶
Bolt sends the status and the headers before the first chunk.
Thus an error in the generator cannot become a 500.
The body ends at the error, and the client gets the chunks before it.
Bolt logs the error with its traceback to the django_bolt.streaming logger, at level ERROR.
Server-Sent Events (SSE)¶
For SSE endpoints, see the dedicated Server-Sent Events guide. Django-Bolt provides EventSourceResponse with automatic SSE framing, compression skipping, and keep-alive pings:
from collections.abc import AsyncIterable
from django_bolt.responses import EventSourceResponse
@api.get("/events", response_class=EventSourceResponse)
async def events() -> AsyncIterable[dict]:
for i in range(10):
yield {"count": i}
await asyncio.sleep(1)
Response with custom headers¶
Use the Response class for complete control:
from django_bolt import Response
@api.options("/items")
async def options_items():
return Response(
{},
status_code=204,
headers={"Allow": "GET, POST, PUT, DELETE"}
)
Setting cookies¶
Use the .set_cookie() method on any response type:
from django_bolt import Response, JSON
@api.post("/login")
async def login():
return Response({"logged_in": True}).set_cookie("session", "abc123")
Bolt writes the value as Django set_cookie writes it.
A value with a space, a quote, ; or another unsafe character gets double quotes and escapes.
request.cookies and Cookie() remove them again, so a handler reads the value that Bolt sets.
Bolt does not set a value that has a control character or a character above U+00FF, and logs a warning.
Django cannot send these values either.
To keep other text in a cookie, encode it first, for example with urllib.parse.quote().
Cookie options¶
Pass additional options to control cookie behavior:
@api.post("/login")
async def login():
return JSON({"message": "Logged in"}).set_cookie(
name="session",
value="abc123",
max_age=3600, # Expires in 3600 seconds
path="/", # Cookie path (default: "/")
domain=".example.com", # Cookie domain
secure=True, # Only send over HTTPS
httponly=True, # Not accessible via JavaScript
samesite="Lax", # CSRF protection: "Strict", "Lax", or "None"
)
Cookie attributes:
| Attribute | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Cookie name |
value |
str |
"" |
Cookie value |
max_age |
int |
None |
Seconds until expiration |
expires |
datetime \| str |
None |
Expiration date |
path |
str |
"/" |
Cookie path |
domain |
str |
None |
Cookie domain |
secure |
bool |
False |
HTTPS only |
httponly |
bool |
False |
No JavaScript access |
samesite |
str \| False |
"Lax" |
"Strict", "Lax", "None", or False to omit |
Multiple cookies¶
Chain multiple .set_cookie() calls:
@api.post("/login")
async def login():
return Response({"logged_in": True}) \
.set_cookie("session", "abc123", httponly=True) \
.set_cookie("preferences", "dark-mode", max_age=86400 * 365)
Deleting cookies¶
Use .delete_cookie() to remove a cookie:
@api.post("/logout")
async def logout():
return Response({"logged_out": True}).delete_cookie("session")
With path and domain (must match the original cookie):
@api.post("/logout")
async def logout():
return Response({"logged_out": True}).delete_cookie(
"session",
path="/app",
domain=".example.com"
)
A name that starts with __Secure- or __Host- gets the Secure flag, as in Django.
A browser ignores the delete of such a cookie without this flag.
Set and delete in one response¶
@api.post("/refresh")
async def refresh():
return Response({"refreshed": True}) \
.delete_cookie("old_session") \
.set_cookie("new_session", "xyz789", httponly=True)
Cookies on all response types¶
The .set_cookie() and .delete_cookie() methods work on all response types:
from django_bolt import Response, JSON, StreamingResponse
from django_bolt.responses import HTML, PlainText, Redirect
# JSON
JSON({"ok": True}).set_cookie("api_token", "secret")
# HTML
HTML("<h1>Welcome</h1>").set_cookie("visited", "true")
# PlainText
PlainText("OK").set_cookie("status", "checked")
# Redirect with cookie (e.g., after login)
Redirect("/dashboard").set_cookie("just_logged_in", "1", max_age=5)
# Streaming response
StreamingResponse(generate()).set_cookie("stream_id", "12345")
Using the Cookie class directly¶
For advanced use cases, create Cookie objects directly:
from django_bolt import Cookie
cookie = Cookie(
name="session",
value="abc123",
max_age=3600,
secure=True,
httponly=True,
samesite="Strict",
)
# Get the Set-Cookie header value
header_value = cookie.to_header_value()
# "session=abc123; Max-Age=3600; Path=/; Secure; HttpOnly; SameSite=Strict"
Response validation¶
Validate response data against a schema using response_model:
import msgspec
class User(msgspec.Struct):
id: int
username: str
email: str
@api.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int):
return {"id": user_id, "username": "john", "email": "john@example.com"}
If the response doesn't match the schema, a 500 error is returned.
You can also use return type annotations:
@api.get("/users/{user_id}")
async def get_user(user_id: int) -> User:
return {"id": user_id, "username": "john", "email": "john@example.com"}
The schema describes the body of the default status code of the route. Bolt validates a JSON or Response only when its status code is that default. A response with another status code, such as a 400 with a map of field errors, is encoded as-is. To document other status codes, use the per-status-code form below. It does not validate them either.
@api.post("/users", status_code=201)
async def create_user(body: UserCreate) -> User:
if await username_taken(body.username):
return JSON({"username": ["Already taken."]}, status_code=400) # not validated
return JSON(await create(body), status_code=201) # validated as User
Per-status-code response schemas¶
response_model also accepts a dict mapping status codes to types. Each code gets its own OpenAPI response entry. Bolt encodes the body as-is and does not validate it.
import msgspec
class Item(msgspec.Struct):
id: int
name: str
class Error(msgspec.Struct):
detail: str
@api.get("/items/{item_id}", response_model={200: Item, 404: Error})
async def get_item(item_id: int):
item = await find_item(item_id)
if item is None:
return 404, {"detail": "Item not found"}
return 200, {"id": item.id, "name": item.name}
Tuple return — Return (status_code, data) to select the schema by code:
return 200, {"id": 1, "name": "Alice"} # documented as Item
return 404, {"detail": "Not found"} # documented as Error
JSON() return — JSON(data, status_code=...) also selects the schema:
Bare return — A plain dict or list uses the default status code (lowest 2xx in the map):
@api.get("/items", response_model={200: list[Item], 400: Error})
async def list_items():
return [{"id": 1, "name": "Alice"}] # documented as list[Item] at 200
204 No Content — Use {204: None} with return (204, None) for empty body responses:
@api.delete("/items/{item_id}", response_model={204: None, 404: Error})
async def delete_item(item_id: int):
deleted = await try_delete(item_id)
if not deleted:
return 404, {"detail": "Item not found"}
return 204, None
Ellipsis catch-all — Use ... as a key to document any unmapped status code:
@api.get("/items/{item_id}", response_model={200: Item, ...: Error})
async def get_item(item_id: int):
... # any non-200 status code is documented as Error
Explicit status_code — Override the auto-detected default status code:
@api.post("/items", response_model={201: Item, 400: Error}, status_code=201)
async def create_item():
return {"id": 1, "name": "New"} # bare return uses 201 instead of auto-detected
Typed error bodies — Raise HTTPException with body= to send a typed body from anywhere. The handler return annotation stays the success type, so type checkers can check it:
@api.post("/plans/{pk}/start", response_model={200: Plan, 400: Error})
async def start_plan(request, pk: int) -> Plan:
if not healthy:
raise HTTPException(400, body=Error(detail="not healthy"))
return Plan(id=pk)
body replaces the default {"detail": ...} envelope. Bolt encodes it as-is and does not validate it. An unmapped status code is sent as-is too.
Setting default status codes¶
Set a default status code for an endpoint:
OpenAPI documents this status code even when the handler has no response annotation.
Returning strings and bytes¶
Returning a string creates a plain text response:
Returning bytes creates an octet-stream response:
@api.get("/bytes")
async def bytes_response():
return b"binary data" # Content-Type: application/octet-stream
Empty responses¶
For 204 No Content responses: