Request and context

This page documents the per-request Context (c) and inbound Request (c.req). Registration, middleware, and c.route are on Routing. Outbound bytes, Writer, and cookie writes are on Responses.

Import Request from stario.http:

python
from stario.http import Request

Context

Context is the per-request bundle the framework constructs before your handler runs. You do not instantiate it yourself.

Attribute / methodPurpose
appThe App instance handling this request (create_task, shutdown signaling).
reqRequest: method, path, headers, lazy query/cookies, body readers.
spanRequest telemetry span (started/ended around App.__call__). Use c.span.attr, event, fail for dimensions and errors.
stateMutable dict[str, Any]. Middleware stashes values; inner layers and the handler read them.
routeRouteMatch: canonical pattern string and captured segments. See Route parameters. Empty pattern on 404 / some 405 paths — check before using params.
disconnectasyncio.Future that completes when the client closes this connection.
disconnectedTrue when disconnect has completed.
shutting_downTrue when the server has begun draining this app (same moment as app.shutting_down).
closingTrue 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.

python
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(app, req, span, _disconnect, state=<factory>, route=RouteMatch(pattern='', params=mappingproxy({})))

Per-request bundle passed to every handler and middleware (routing fills route before the handler runs).

Fields

  • app(App):The App instance for this request.
  • req(Request):Parsed HTTP request (method, path, headers, body reader).
  • span(Span):Telemetry span for this request; started/ended by the app callable.
  • _disconnect(Future):Completes when the client closes this request's connection.
  • state(dict):Mutable dict for middleware to pass data to inner layers and the handler. default: factory
  • route(RouteMatch):Filled by App.__call__ before the handler runs; do not assign in handlers. default: RouteMatch(pattern='', params=mappingproxy({}))

Context.alive(source=None)

Watch client disconnect and app shutdown; cancel this task when either happens.

Use async with c.alive(): ... for scoped work, or async for item in c.alive(source): ... to stream from source until disconnect or shutdown. Do not use async for without source; the context-manager form is the supported no-source pattern.

class RouteMatch(pattern, params)

Result of routing: a canonical pattern string plus captured path/host segments.

Fields

  • pattern(str):Matched route template (useful for logs), including host part when present.
  • params(Mapping):Map from {param} / {rest...} names to decoded segment text.

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

MemberHow you access itNotes
HTTP methodc.req.methodstr (e.g. "GET"). Fixed when the request starts.
Pathc.req.pathPath only; no query string. Fixed at start.
Headersc.req.headersHeaders — case-insensitive keys; get, getlist, items, etc.
Protocolc.req.protocol_versionstr, e.g. "1.1".
Keep-alivec.req.keep_alivebool.
Hostc.req.hostHost header without port, lowercased; IPv6 keeps [...]. Lazy on first read. "" if missing.
Query stringc.req.queryParsedQuery. Lazy. Parsed from the URL ?… portion.
Raw query bytesc.req.query_bytesBytes after ? (empty if none). Use when you parse or validate the query yourself.
Cookiesc.req.cookiesRead-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-Length exceeds the configured maximum, the protocol rejects the message with 413 before your handler runs.

  • HttpException(413) if the body exceeds the configured maximum size while reading (default 10 MiB).

  • HttpException(408) if idle between chunks exceeds the body read timeout (default 30 seconds).

  • ClientDisconnected if the peer closes before the body completes. App aborts the connection without sending a response body.

Pick one of body() or stream() per request — switching in either direction raises RuntimeError. You may call body() multiple times after the first read returns the same bytes. A second stream() on the same request also raises RuntimeError.

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.

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 as get). Pass last=True to 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(*, method='GET', path='/', query_bytes=b'', protocol_version='1.1', keep_alive=True, headers, body)

Stable snapshot of request-line data plus headers; body I/O goes through an internal BodyReader.

query_bytes is the raw ?-suffix from the URL (no leading ?). host, query, and cookies are computed lazily on first access. Do not reassign query_bytes after construction or mutate the returned cookies dict in place — both desynchronize the cached views.

async Request.body(max_size=None)

Return the entire body as bytes (empty if there is no body reader).

Internally uses BodyReader.read on the protocol-owned reader (size cap, timeouts, backpressure).

  • max_size: Optional lower per-call limit. The server's configured maximum body size still applies.

  • HttpException (413): When the body exceeds the configured maximum size.

  • HttpException (408): When bytes stall longer than the body read timeout (slow upload / slowloris guard).

  • ClientDisconnected: When the peer closes before the request body finishes uploading.

  • StarioRuntime: If the body was already streamed via stream().

async Request.stream()

Stream the body; mutually exclusive with body() for a given request.

Internally uses BodyReader.stream.

  • HttpException (413 / 408): For oversize or stalled uploads (same rules as body()).

  • ClientDisconnected: When the peer closes before the request body finishes uploading.

  • StarioRuntime: If stream() or body() already consumed this body.

class ParsedQuery(raw)

View over parsed query bytes preserving repeated keys.

Use get for the first value, getlist / as_lists for every value, and as_dict for one string per key (Pydantic-friendly).

ParsedQuery.as_dict(*, last=False)

One string per key, suitable for Pydantic model_validate and similar.

Repeated keys (?a=1&a=2) keep the first value by default (same as get). Pass last=True to keep the last value. For every value as a list, use as_lists.

ParsedQuery.as_lists()

All keys with every repeated value preserved (copy of each list).

Use with schemas whose fields are list[str] (or similar) for ?tag=a&tag=b-style parameters.

ParsedQuery.get(key, default=None)

First value for key, or default when the key is absent.

ParsedQuery.getlist(key)

Every value for key (empty list if missing), preserving duplicates from the query string.

ParsedQuery.items()

All key-value pairs, flattened.

class Headers(raw_header_data=None)

Case-insensitive HTTP headers with a bytes-backed internal store.

_data maps lowercased header-name bytes to either one value (bytes) or multiple values (list[bytes]). The single-value shape keeps common headers cheap; the list shape preserves duplicates such as Set-Cookie.

_data is a stable internal layout for protocol/writer hot paths.

Headers.add(name, value)

Append a validated value, keeping prior values for the same name.

Headers.get(name, default=None)

Return the first value as str, or default when missing.

Headers.getlist(name)

Return every value for name as strings.

Headers.items()

Return flattened (name, value) pairs as lowercased strings.

Headers.remove(name)

Remove all values for name.

Headers.set(name, value)

Replace all values for name with one validated value.

Headers.setdefault(name, value)

Return the first value, or set and return value when absent.

Headers.unsafe_add(name, value)

Append wire bytes; name must already be lowercased.

Headers.unsafe_append_wire_lines(parts)

Append header lines to parts for protocol/writer hot paths.

Headers.unsafe_get(name, default=None)

Return the first wire value as bytes.

Headers.unsafe_getlist(name)

Return every wire value for name as bytes (copy prevents aliasing).

Headers.unsafe_items()

Return flattened (name, value) pairs as wire bytes.

Headers.unsafe_iter_wire_pairs()

Return (name, value) wire pairs without flattening multi-value headers.

Headers.unsafe_remove(name)

Remove all wire values for name.

Headers.unsafe_set(name, value)

Set wire bytes for name; no validation.