Skip to the content.

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

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