Context (c) is the per-request bundle your handler receives, and c.req is the inbound Request. Registration, middleware, and c.match are on Routing; outbound bytes are on Responses.
Import Request from stario.http:
from stario.http import RequestContext#
Context is the per-request bundle the framework constructs before your handler runs. You do not instantiate it yourself.
| Attribute / method | Purpose |
|---|---|
app | The App instance handling this request (create_task, shutdown signaling). |
req | Request: method, path, headers, lazy query/cookies, body readers. |
span | Request telemetry span (covers routing, middleware, and your handler). Use c.span.attr, event, fail for dimensions and errors. |
state | Mutable dict[str, Any]. Middleware stashes values; inner layers and the handler read them. |
match | Immutable Match: .pattern is METHOD plus the URL template; .params are captured {name} / {name...} values. Match.empty on 404 and 405. See Route parameters. |
disconnect | asyncio.Future that completes when the client closes this connection. |
disconnected | True when disconnect has completed. |
shutting_down | True when the server has begun draining this app (same moment as app.shutting_down). |
closing | True when disconnected or shutting_down — handler work should stop. |
alive() | Connection lifecycle helper; see below. |
Send the response with w (Writer); read the inbound message from c.req.
alive()#
Long handlers (SSE, slow streams, relay subscribers) should listen for client disconnect and process shutdown. c.closing supports explicit checkpoint loops. async with c.alive(): installs a watcher that cancels the current task when disconnect or app.shutdown completes first (whichever happens first). async for item in c.alive(source): forwards items from an async iterable until the same cancellation path fires.
Exiting alive() suppresses CancelledError only when cancellation came from this watcher, not from unrelated cancellation elsewhere. Do not use async for c.alive() without a source — use the context manager form.
For response I/O during drain, Writer may still write until disconnected is true even when closing is already true.
See Responses for writer phases and Runtime for shutdown timing.
import asyncio from stario import Context, Writer async def long_poll(c: Context, w: Writer) -> None: async with c.alive(): while True: await asyncio.sleep(0.5) # Task is cancelled when the client drops or the server shuts down.class Context(*args, **kwargs)#
Per-request bundle passed to every handler and middleware.
Context.alive(source=None)#
Watch client disconnect and app shutdown; cancel this task when either happens.
class Route(*parts, method=None, host=None, path=None)#
One HTTP method on one path template.
HOME = Route("GET /")SEND = Route("POST", ROOM + "/send") app.add(SEND, send)at.post(SEND.href(room.id))Route.empty(cls)#
Unmatched 404 / 405 sentinel. Do not register it.
Route.href(*args, query=None, fragment=None, **params)#
Browser URL string. Placeholders are strings; all must be provided.
Request#
Stable view of the request line and headers. The protocol builds it; handlers only read. Reach everything through c.req.
Request fields and accessors#
| Member | How you access it | Notes |
|---|---|---|
| HTTP method | c.req.method | str (e.g. "GET"). Fixed when the request starts. |
| Path | c.req.path | Path only; no query string. Fully percent-decoded. Fixed at start. |
| Raw path | c.req.raw_path | Exact percent-encoded path without the query; routing uses this form. |
| Headers | c.req.headers | RequestHeaders — read-only, case-insensitive; get, getlist, items, etc. |
| Protocol | c.req.protocol_version | str, e.g. "1.1". |
| Keep-alive | c.req.keep_alive | bool. |
| Host | c.req.host | Host header without port, lowercased; IPv6 keeps [...]. Lazy on first read. "" if missing. |
| Query string | c.req.query | ParsedQuery. Lazy. Parsed from the URL ?… portion. |
| Raw query bytes | c.req.query_bytes | Bytes after ? (empty if none). Use when you parse or validate the query yourself. |
| Cookies | c.req.cookies | Read-only Mapping[str, str] merged from Cookie headers. Lazy; do not mutate in place. |
| Body (buffered) | await c.req.body() | Full body as bytes. Size cap and timeouts from Configuration. |
| Body (stream) | async for chunk in c.req.stream(): | Chunked iterator; uses server max_body_bytes and body_timeout (same 413/408/ClientDisconnected). No per-call max_size. |
Body rules#
On body() and stream():
If
Content-Lengthexceeds the configured maximum, the protocol rejects the message with 413 before your handler runs.RequestBodyError(413)if the body exceeds the configured maximum size while reading (default 10 MiB).RequestBodyError(408)if idle between chunks exceeds the body read timeout (default 30 seconds). BothRequestBodyErrorstatuses answer themselves with the status and a plain body when nothing was sent.ClientDisconnectedif the peer closes before the body completes. The connection is aborted without a response body.
Pick one of body() or stream() per request — switching in either direction raises StarioRuntime. You may call body() multiple times after the first read returns the same bytes. A second stream() on the same request also raises StarioRuntime.
Requests with no body (typical GET): body() returns b""; stream() yields no chunks.
Per-route smaller limits: await c.req.body(max_size=65536) only on body() — stream() relies on the server-wide STARIO_REQUESTS_MAX_BODY_BYTES unless you count bytes in the loop.
Forms and JSON bodies#
The body is bytes. Stario does not parse it for you, because the standard library already does. A posted HTML form is application/x-www-form-urlencoded:
from urllib.parse import parse_qs form = parse_qs((await c.req.body()).decode())email = form.get("email", [""])[0]A JSON body goes through the same codec w.json uses:
import stario.json as stario_json payload = stario_json.loads(await c.req.body())Validate the result with the library you already use. A Datastar request carries its signals as JSON; read those with read_signals on Datastar. Multipart uploads are on User uploads.
ParsedQuery#
c.req.query is a ParsedQuery, not a flat dict. Repeated keys are preserved.
query.get("key")— first value or default.query.getlist("key")— every value for the key.query.as_dict()— one string per key; first value wins by default (same asget). Passlast=Trueto keep the last value.query.as_lists()— full multi-value map.
Use as_dict() or as_lists() when validating the whole query (for example with Pydantic).
Cookie response headers use stario.cookies with w (Cookies).
class Request(*args, **kwargs)#
One HTTP request. New object per dispatch; not pooled with the exchange.
Headers, query, and host read the exchange arena while the handler runs. If the Request (or its headers / query / cookies) is still referenced when the exchange is recycled, it takes a private copy, so it stays valid after the handler returns. The body is only readable while the handler runs, unless it was already read.
async Request.body(max_size=None)#
async Request.stream(max_chunk=None)#
class ParsedQuery(*args, **kwargs)#
Query index. First read fills C name/value spans.
Names that need no unquote stay on the original bytes. Names with + / %XX get a private copy so they can decode in place. get memcmps typical ASCII names, then decodes only that value. First name= wins. items / as_dict / as_lists decode everything.
ParsedQuery.as_dict(*, last=False)#
ParsedQuery.as_lists()#
ParsedQuery.get(key, default=None)#
ParsedQuery.getlist(key)#
ParsedQuery.items()#
stario.http.RequestHeaders#
Read-only request headers (c.req.headers).
While the handler runs this reads the exchange arena in place. When the exchange is recycled and this view is still referenced, it copies the arena and header table so it keeps answering for this request.
RequestHeaders.add(name, value)#
RequestHeaders.get(name, default=None)#
Value of name, else default. A name no request can carry is absent.