Request & Response
Every route handler receives a Request and a Response. The Request
carries the parsed HTTP data; the Response writes the framed HTTP reply back
to the client.
The Request object
Request is a dataclass with these attributes:
| Attribute | Type | Description |
|---|---|---|
method |
str |
HTTP method, e.g. "GET" |
path |
str |
Request path without the query string, e.g. /api/users |
url |
str |
Full path including the query string |
http_version |
str |
e.g. "HTTP/1.1" |
headers |
dict[str, str] |
Headers, lowercased keys |
query_params |
dict[str, list[str]] |
Query parameters (multi-valued) |
cookies |
dict[str, str] |
Parsed cookies |
body |
str |
Raw request body |
user_parameters |
dict[str, str] \| None |
Path parameters from the matched route |
The json property decodes the body when it is valid JSON, otherwise None:
from axion import WebServer
from axion.http import Request, Response
app = WebServer(debug=True)
@app.route("/echo", methods=["POST"])
def echo(request: Request, response: Response):
data = request.json
response.json({"received": data})
@app.route("/search")
def search(request: Request, response: Response):
terms = request.query_params.get("q", [])
response.json({"terms": terms})
The keep_alive property reports whether the client requested a persistent
connection (used internally by the connection loop).
The Response object
The Response owns the header state and writes the full response (status
line, headers, body) to the socket.
Sending content
response.send("Hello, world!") # text/plain, status 200
response.send("<h1>Hi</h1>") # text/html (auto-detected)
response.send({"key": "value"}) # JSON
response.send(b"\x00\x01", status=201) # raw bytes
bytesbodies default toapplication/octet-stream.dict/listbodies are JSON-encoded withapplication/json.- Other objects are converted with
str()and sent as plain text.
JSON responses
@app.route("/api/user")
def user(response: Response):
response.json({"id": 1, "name": "Alice"}, status=200)
Rendering templates
@app.route("/")
def index(response: Response):
response.render("index.html", title="Home", user="Alice")
render renders a Jinja2 template (autoescaping is on) from
TEMPLATE_DIRS and sends it as HTML. A missing template
produces a 404 with the body Template Not Found.
Templates are resolved through all directories in TEMPLATE_DIRS; pass a list
to render the first one that exists. You can also pass a pre-built
jinja2.Template object.
Raw bytes
@app.route("/report.pdf")
def report(response: Response):
response.send_raw(pdf_bytes, content_type="application/pdf")
Headers
response.set_header("X-Frame-Options", "DENY")
response.set_header("Cache-Control", "no-store")
By default every response includes X-Content-Type-Options: nosniff and
Connection set from the negotiated keep-alive state. Use set_header to add
or replace any header.
Cookies
response.add_cookie("session", token, path="/", http_only=True, max_age=3600)
add_cookie emits a Set-Cookie header. Reuse it to set multiple cookies
(they are merged into separate Set-Cookie lines). http_only=True by
default.
Status code
Every send method accepts status=:
response.send("Created", 201)
response.json({"error": "bad request"}, status=400)
After a response has been sent, response.status_code holds the last status
code. The special connection-management statuses are handled by the connection
loop: 304 (empty body, no Content-Length) and 101 (switching protocols).
No response sent?
Handlers may return early without sending. axion waits for a response; any sending method marks the request as answered, and the connection loop stops waiting once a response has been written.
Next steps
- Read about middleware hooks that run before and after your handlers.
- See routing for path parameters and error handlers.