Skip to content

without_web

An opinionated HTTP/WebSocket router for without-asgi: trie matching, typed path params, 405-vs-404, mounting, and OpenAPI.

without_web

FLOAT module-attribute

FLOAT: Converter[float] = Converter(
    name="float", parse=float, schema={"type": "number"}
)

INT module-attribute

INT: Converter[int] = Converter(
    name="int", parse=int, schema={"type": "integer"}
)

PATH module-attribute

PATH: Converter[str] = Converter(
    name="path", parse=str, schema={"type": "string"}
)

STR module-attribute

STR: Converter[str] = Converter(
    name="str", parse=str, schema={"type": "string"}
)

UUID module-attribute

UUID: Converter[UUID] = Converter(
    name="uuid",
    parse=uuid.UUID,
    schema={"type": "string", "format": "uuid"},
)

delete module-attribute

delete = _Method('DELETE')

get module-attribute

get = _Method('GET')

head module-attribute

head = _Method('HEAD')

options module-attribute

options = _Method('OPTIONS')

patch module-attribute

patch = _Method('PATCH')

post module-attribute

post = _Method('POST')

put module-attribute

put = _Method('PUT')

ExceptionRecover

ExceptionRecover = Callable[
    [Exception], Awaitable[Response | None]
]

WebsocketExceptionRecover

WebsocketExceptionRecover = Callable[
    [Exception], Awaitable[WebsocketClose | None]
]

Reply

Returned

Returned = Awaitable[Reply] | Stream[Outbound]

WebsocketReturned

WebsocketReturned = Stream[WebsocketOutbound]

SchemaFor

SchemaFor = Callable[[type], Mapping[str, object]]

SchemaRef

SchemaRef = Mapping[str, object] | type

Segment

Segment = Literal | Param | CatchAll

Endpoint

Endpoint = Callable[[T, Match[S]], H]

HttpEndpoint

HttpEndpoint = Endpoint[T, HttpScope, HttpHandler]

Pattern

Pattern = str | Template

WebsocketEndpoint

WebsocketEndpoint = Endpoint[
    T, WebsocketScope, WebsocketHandler
]

Converter dataclass

Converter(
    name: str,
    parse: Callable[[str], _V_co],
    schema: Mapping[str, object],
)

Bases: Generic[_V_co]

A path-segment parser paired with the JSON Schema it parses into.

parse turns a single matched segment into a typed value, raising ValueError to reject a segment that does not fit (int against "abc"). Rejection is not a handler-side error: it makes that trie branch fail to match so the walk backtracks to a sibling, ultimately a 404 if nothing matches (parse, don't validate).

schema is the half the router contributes to OpenAPI for a path parameter that uses this converter: the router owns the path-param schema because it owns the converter.

name is the converter's identity (and its OpenAPI parameter style); a typed-token pattern reuses the converter value directly (path_param("id", INT)), so the name, parse, type, and schema are all declared in one place. Equality and hashing are by name alone (a converter is a trie key), so parse and schema are excluded from comparison.

name instance-attribute

name: str

parse class-attribute instance-attribute

parse: Callable[[str], _V_co] = field(compare=False)

schema class-attribute instance-attribute

schema: Mapping[str, object] = field(compare=False)

BufferedRequest dataclass

BufferedRequest(
    scope: HttpScope,
    path_params: Mapping[str, object],
    query_params: Mapping[str, tuple[str, ...]],
    body: bytes,
)

Bases: HttpRequestHead

An HttpRequestHead plus the fully-buffered request body, built only on the buffered-HTTP path. The body extractor's context is exactly this type, so a body token on a streaming or websocket route (which build a bodyless HttpRequestHead/WebsocketRequestHead) is a static type error, not a runtime guard.

body instance-attribute

body: bytes

buffered classmethod

buffered(
    scope: HttpScope,
    path_params: Mapping[str, object],
    body: bytes,
) -> BufferedRequest

Assemble a BufferedRequest, parsing the query string and carrying the buffered body.

ExtractionError

ExtractionError(
    message: str,
    *,
    field: str | None = None,
    cause: Exception | None = None,
)

Bases: ValueError

A request rejected while one of its typed values was being extracted.

The reject signal an extractor raises when a parse (a once/optional cardinality check, a converter, a pydantic model) refuses the input. It gathers at the raise site everything a recover policy needs to answer well: field names the request part that failed (a query/header parameter name, or None for the body), and cause carries the underlying error as a first-class value, so a policy matches case ExtractionError(cause=ValidationError()) to answer a 422 for an invalid body versus a 400 for a bad query/header value, without reaching into __cause__.

A ValueError is the codebase's "reject" signal (the same one a converter raises to backtrack the trie walk), so the extractors turn one into a rich ExtractionError, and the router wraps any stray one left unattributed. Making the boundary a single matchable type is what lets a plain ValueError raised deeper in a handler still surface as a 500 rather than masquerading as a client error.

field instance-attribute

field = field

cause instance-attribute

cause = cause

Extractor dataclass

Extractor(
    extract: Callable[[_C_contra], _V_co],
    query: tuple[QueryParam, ...] = (),
    headers: tuple[HeaderParam, ...] = (),
    request_body: Body | None = None,
    path: PathSpec | None = None,
)

Bases: Generic[_C_contra, _V_co]

A typed piece of a request, paired with the OpenAPI it contributes.

extract is a pure C -> V (where C is the request context it reads: the permissive RequestHead, or a narrower HttpRequestHead/WebsocketRequestHead/ BufferedRequest) that raises to reject a bad request (a catching middleware's recover maps the raised type to a 4xx); it never decides which handler runs. The context parameter is what lets a handler refuse the wrong extractor for its route kind statically (a body on a streaming route, an http_scope on a websocket). The same value carries its own OpenAPI fragment, so a handler's parameter list and request body are recovered from the extractors it declares, never restated: one declaration, two consumers (parse and describe).

extract instance-attribute

extract: Callable[[_C_contra], _V_co]

query class-attribute instance-attribute

query: tuple[QueryParam, ...] = ()

headers class-attribute instance-attribute

headers: tuple[HeaderParam, ...] = ()

request_body class-attribute instance-attribute

request_body: Body | None = None

path class-attribute instance-attribute

path: PathSpec | None = None

HttpRequestHead dataclass

HttpRequestHead(
    scope: HttpScope,
    path_params: Mapping[str, object],
    query_params: Mapping[str, tuple[str, ...]],
)

Bases: RequestHead

A RequestHead whose scope is known to be an HttpScope: the context of any HTTP route (buffered or streaming). http_scope() reads its narrowed scope with no runtime check, and the streaming-HTTP path builds it directly.

scope instance-attribute

scope: HttpScope

parsed classmethod

parsed(
    scope: HttpScope, path_params: Mapping[str, object]
) -> HttpRequestHead

Assemble an HttpRequestHead, parsing the scope's query string once at the boundary.

RequestHead dataclass

RequestHead(
    scope: HttpScope | WebsocketScope,
    path_params: Mapping[str, object],
    query_params: Mapping[str, tuple[str, ...]],
)

The parsed head of a request (or websocket handshake): everything an extractor reads except the body. The read-only, parsed-once context handed to each scope-derived extractor, and the most general context in the lattice below: path_param, catch_all, query_param, and header_param read only what is here, so they work on any route.

path_params holds the path parameters the router already parsed during the trie walk (typed values, stored as object); query_params is the query string decoded and parsed once (via parse_qs), each name's values held as an immutable tuple, so a handler declaring N query_param tokens shares one parse rather than re-decoding the query string per token. scope is HttpScope | WebsocketScope so a query_param/header_param token reads either (both carry query_string and headers); the whole-scope read is split into the protocol-specific http_scope()/websocket_scope(), since only those know the concrete type. A header_param reads the scope's raw header pairs directly through the without_asgi.headers functions.

The subtypes narrow the two facts a route fixes, so the wrong extractor on the wrong route is a static error rather than a runtime guard (see Extractor):

  • HttpRequestHead narrows scope to HttpScope (what http_scope() needs).
  • WebsocketRequestHead narrows scope to WebsocketScope (websocket_scope()).
  • BufferedRequest (an HttpRequestHead) adds the buffered body (body()).

Each route builds exactly its concrete context: the buffered-HTTP path a BufferedRequest, the streaming-HTTP path an HttpRequestHead, a websocket a WebsocketRequestHead. This base is never built directly; it names the shared top so the permissive extractors can slot into all three.

scope instance-attribute

path_params instance-attribute

path_params: Mapping[str, object]

query_params instance-attribute

query_params: Mapping[str, tuple[str, ...]]

WebsocketRequestHead dataclass

WebsocketRequestHead(
    scope: WebsocketScope,
    path_params: Mapping[str, object],
    query_params: Mapping[str, tuple[str, ...]],
)

Bases: RequestHead

A RequestHead whose scope is known to be a WebsocketScope: the context of a websocket route. websocket_scope() reads its narrowed scope with no runtime check, and the websocket path builds it directly.

scope instance-attribute

parsed classmethod

parsed(
    scope: WebsocketScope, path_params: Mapping[str, object]
) -> WebsocketRequestHead

Assemble a WebsocketRequestHead, parsing the scope's query string once at the boundary.

Body dataclass

Body(media_type: str, shape: Shape)

One content entry: a media type paired with the shape of its payload.

The same value describes a request or a response body. Single renders a schema; Sequence renders an itemSchema.

media_type instance-attribute

media_type: str

shape instance-attribute

shape: Shape

Describable

Bases: Protocol

describe

describe() -> RouteSpec

HeaderParam dataclass

HeaderParam(
    name: str, schema: SchemaRef, required: bool = False
)

name instance-attribute

name: str

schema instance-attribute

schema: SchemaRef

required class-attribute instance-attribute

required: bool = False

QueryParam dataclass

QueryParam(
    name: str, schema: SchemaRef, required: bool = False
)

name instance-attribute

name: str

schema instance-attribute

schema: SchemaRef

required class-attribute instance-attribute

required: bool = False

ResponseSpec dataclass

ResponseSpec(
    description: str = "", body: Body | None = None
)

description class-attribute instance-attribute

description: str = ''

body class-attribute instance-attribute

body: Body | None = None

RouteSpec dataclass

RouteSpec(
    summary: str = "",
    query: tuple[QueryParam, ...] = (),
    headers: tuple[HeaderParam, ...] = (),
    request_body: Body | None = None,
    responses: Mapping[int, ResponseSpec] = dict(),
)

The handler-owned half of an endpoint's OpenAPI description.

The router never sees the body or interprets the query, so it cannot be the source of those schemas: an endpoint declares them here, in the one place they are also parsed. openapi merges this with the router's path/method/path-param half.

summary class-attribute instance-attribute

summary: str = ''

query class-attribute instance-attribute

query: tuple[QueryParam, ...] = ()

headers class-attribute instance-attribute

headers: tuple[HeaderParam, ...] = ()

request_body class-attribute instance-attribute

request_body: Body | None = None

responses class-attribute instance-attribute

responses: Mapping[int, ResponseSpec] = field(
    default_factory=dict
)

Sequence dataclass

Sequence(item_schema: SchemaRef)

Body content that is a sequence of items, each validating against item_schema.

Renders as OpenAPI 3.2's itemSchema, the description of a sequential media type (NDJSON, JSON Lines, application/json-seq, SSE text/event-stream, multipart/mixed, ...). without-web is agnostic to the framing on the wire: the application names it via Body.media_type and emits the bytes itself. This is documentation only; nothing on the runtime path reads it.

item_schema instance-attribute

item_schema: SchemaRef

Single dataclass

Single(schema: SchemaRef)

Body content that is one complete document validating against schema.

Renders as OpenAPI's schema: the whole request/response body is this value.

schema instance-attribute

schema: SchemaRef

CatchAll dataclass

CatchAll(name: str, converter: Converter[object])

A parameter that consumes the rest of the target; always the last segment.

name instance-attribute

name: str

converter instance-attribute

converter: Converter[object]

Literal dataclass

Literal(text: str)

A segment matched verbatim.

text instance-attribute

text: str

Param dataclass

Param(name: str, converter: Converter[object])

A single-segment typed parameter, carrying the converter that parses it.

name instance-attribute

name: str

converter instance-attribute

converter: Converter[object]

PathSpec dataclass

PathSpec(
    name: str,
    converter: Converter[object],
    catch_all: bool = False,
)

How a path-param extractor appears as a route segment.

The bridge that lets one path_param(...) value be both a pattern segment (the router matches and schemas it through converter) and a typed read in the handler. name binds the segment; catch_all marks the rest-consuming form.

name instance-attribute

name: str

converter instance-attribute

converter: Converter[object]

catch_all class-attribute instance-attribute

catch_all: bool = False

Delegate dataclass

Delegate(prefix: str, target: HttpRouter[T])

An opaque HTTP sub-application delegated to at a literal-string prefix.

The bring-your-own-app escape hatch: target (another HttpRouter, a legacy app) is handed the prefix-trimmed scope (ASGI root_path semantics) and treated as a black box, since its routes cannot be seen, baked, or reversed. Transparent sub-apps whose routes you own use mount(...) instead, which bakes the prefix into the routes so they stay first-class values. The prefix is a plain str, hence a literal path with no parameter to bind.

prefix instance-attribute

prefix: str

target instance-attribute

target: HttpRouter[T]

Match dataclass

Match(scope: S, params: Mapping[str, object])

What the router hands a handler: the scope plus already-parsed path params.

make_asgi_app's HttpRouter type is unchanged: Router.dispatch still presents as (T, HttpScope) -> HttpHandler. The richer Match is the router's internal endpoint protocol, the one place a handler reads the path parameters the route pattern bound.

scope instance-attribute

scope: S

params instance-attribute

params: Mapping[str, object]

Reversible

Bases: Protocol

Anything reverse routing can render: a value carrying parsed path segments.

Both Route and WebsocketRoute satisfy it structurally, so url_for takes either without naming the lifespan-state type they are generic over (which would otherwise fight variance).

segments property

segments: tuple[Segment, ...]

Route dataclass

Route(
    segments: tuple[Segment, ...],
    methods: Mapping[str, HttpEndpoint[T]],
)

A route: parsed path segments bound to one endpoint per HTTP method.

The method-decorator form (@get(...)) produces a single-method Route; the Router merges Routes that share a path into one method map, so the 405-vs-404 split still falls out of the trie. segments is the complete path, mount prefixes already baked in (see mount), so a Route is a self-contained value: it reverses (url_for) with no router, and its meaning does not depend on where it is placed.

segments instance-attribute

segments: tuple[Segment, ...]

methods instance-attribute

methods: Mapping[str, HttpEndpoint[T]]

Router dataclass

Router(
    routes: tuple[Route[T] | Delegate[T], ...],
    fallback: HttpEndpoint[T],
    middleware: HttpMiddleware[T] = _PASSTHROUGH_HTTP,
)

An opinionated HTTP router whose dispatch is an HttpRouter[T].

The whole integration surface with without-asgi is that one type: pass router.dispatch as make_asgi_app(http=...) and bring-your-own (or no router at all) stays first-class. The route table is compiled to an immutable trie once at construction; dispatch is then a pure walk that recovers route precedence, 405-vs-404, and delegation from the tree's shape. Routes are flat, self-contained values (mount prefixes are baked in by mount); only opaque Delegates stay as wrappers, since a black box cannot be flattened.

routes instance-attribute

routes: tuple[Route[T] | Delegate[T], ...]

fallback instance-attribute

fallback: HttpEndpoint[T]

middleware class-attribute instance-attribute

middleware: HttpMiddleware[T] = _PASSTHROUGH_HTTP

tree class-attribute instance-attribute

tree: Node[_HttpLeaf[T]] = field(
    init=False, repr=False, compare=False
)

dispatch

dispatch(state: T, scope: HttpScope) -> HttpHandler

WebsocketDelegate dataclass

WebsocketDelegate(prefix: str, target: WebsocketRouter[T])

The WebSocket sibling of Delegate: an opaque WebSocket app at a prefix.

Handed the prefix-trimmed scope and treated as a black box; transparent WebSocket sub-apps use ws_mount(...) to bake the prefix into their routes.

prefix instance-attribute

prefix: str

target instance-attribute

target: WebsocketRouter[T]

WebsocketRoute dataclass

WebsocketRoute(
    segments: tuple[Segment, ...],
    endpoint: WebsocketEndpoint[T],
)

Parsed path segments bound to one WebSocket endpoint (the sibling of Route).

segments instance-attribute

segments: tuple[Segment, ...]

endpoint instance-attribute

endpoint: WebsocketEndpoint[T]

WebsocketRouter dataclass

WebsocketRouter(
    routes: tuple[
        WebsocketRoute[T] | WebsocketDelegate[T], ...
    ],
    fallback: WebsocketEndpoint[T],
    middleware: WebsocketMiddleware[
        T
    ] = _PASSTHROUGH_WEBSOCKET,
)

The WebSocket sibling of Router, reusing the same trie machinery.

There is no method layer, so no 405: a connection either matches a path or falls to the fallback. dispatch is a WebsocketRouter[T] for make_asgi_app(websocket=...). Routes are flat values (prefixes baked by ws_mount); only opaque WebsocketDelegates stay as wrappers.

routes instance-attribute

routes: tuple[
    WebsocketRoute[T] | WebsocketDelegate[T], ...
]

fallback instance-attribute

fallback: WebsocketEndpoint[T]

middleware class-attribute instance-attribute

middleware: WebsocketMiddleware[T] = _PASSTHROUGH_WEBSOCKET

tree class-attribute instance-attribute

tree: Node[_WsLeaf[T]] = field(
    init=False, repr=False, compare=False
)

dispatch

dispatch(
    state: T, scope: WebsocketScope
) -> WebsocketHandler

catching

catching(
    recover: ExceptionRecover,
) -> HttpMiddleware[object]

Build middleware that maps exceptions to a response, before the status commits.

Exception handling is not a new mechanism: it is a Middleware that wraps a handler and watches its outbound stream. recover is the app's policy: it is handed a raised exception and returns the Response to send instead, or None to let the exception propagate. There is deliberately no registry of type -> handler: a recover written as match exc: narrows each case to its real type (no assert isinstance) and can re-raise, chain, or do async work, all of which a heterogeneous mapping could not express without a cast.

Honest limitation: the mapping applies only while the status line can still be set, i.e. until the first ResponseStart flows out. Informational events (EarlyHint, ResponseDebug) precede it and do not commit the status, so an exception after them can still be mapped; once ResponseStart is on the wire the exception re-raises, because the handler can abort but not re-status.

catching_websocket

catching_websocket(
    recover: WebsocketExceptionRecover,
) -> WebsocketMiddleware[object]

The WebSocket sibling of catching, mapping exceptions to a close.

The equivalent commit point is WebsocketAccept: before the handshake is accepted a close still rejects the connection (the server turns it into a 403), so a mapped exception becomes that WebsocketClose. Once accepted the connection is established and the exception re-raises. recover returns the WebsocketClose to send, or None to propagate.

body

body(
    parse: Callable[[bytes], V],
    *,
    schema: SchemaRef,
    media_type: str = "application/json",
) -> Extractor[BufferedRequest, V]

Parse the buffered request body into V.

parse is injected so without-web stays serialization-agnostic: an app passes a pydantic model's model_validate_json, a dataclass loader, or any bytes -> V, and the matching schema is this value's OpenAPI request body. A parse that raises to reject (a pydantic ValidationError, say) becomes an ExtractionError with no field (the body is unnamed), the original on cause.

The buffered body lives on BufferedRequest, so that is this extractor's context: a body token on a bodyless streaming or websocket route is a static type error (its context is not the HttpRequestHead/WebsocketRequestHead those routes provide), not a runtime guard.

catch_all

catch_all(
    name: str, converter: Converter[str] = PATH
) -> Extractor[RequestHead, str]

A typed catch-all path parameter: the {name:path} form as a token.

Consumes the rest of the target into one segment (always the final one); the sibling of path_param for the rest-of-path case.

header_param

header_param(
    name: str,
    parse: Callable[[tuple[bytes, ...]], V],
    *,
    schema: SchemaRef,
    required: bool = False,
) -> Extractor[RequestHead, V]

Parse a request header into V, given all of its raw values.

Header names are matched case-insensitively; parse receives every value sent under name as an immutable tuple, in order, and returns V or raises a ValueError to reject, which becomes an ExtractionError naming this name.

http_scope

Hand an HTTP handler the unparsed HttpScope.

The escape hatch that keeps "pass the scope down" and "parse parts of it" from competing: a handler composes http_scope() alongside parsed extractors and gets the raw connection facts as just another typed argument. Its context is HttpRequestHead, whose scope is already an HttpScope, so it reads it with no runtime check; using it on a websocket route (a WebsocketRequestHead) is a static type error.

into

into(
    make: Callable[[A], M], a: Extractor[R, A]
) -> Extractor[R, M]
into(
    make: Callable[[A, B], M],
    a: Extractor[R, A],
    b: Extractor[R, B],
) -> Extractor[R, M]
into(
    make: Callable[[A, B, C], M],
    a: Extractor[R, A],
    b: Extractor[R, B],
    c: Extractor[R, C],
) -> Extractor[R, M]
into(
    make: Callable[[A, B, C, D], M],
    a: Extractor[R, A],
    b: Extractor[R, B],
    c: Extractor[R, C],
    d: Extractor[R, D],
) -> Extractor[R, M]
into(
    make: Callable[[A, B, C, D, E], M],
    a: Extractor[R, A],
    b: Extractor[R, B],
    c: Extractor[R, C],
    d: Extractor[R, D],
    e: Extractor[R, E],
) -> Extractor[R, M]
into(
    make: Callable[[A, B, C, D, E, F], M],
    a: Extractor[R, A],
    b: Extractor[R, B],
    c: Extractor[R, C],
    d: Extractor[R, D],
    e: Extractor[R, E],
    f: Extractor[R, F],
) -> Extractor[R, M]
into(
    make: Callable[[A, B, C, D, E, F, G], M],
    a: Extractor[R, A],
    b: Extractor[R, B],
    c: Extractor[R, C],
    d: Extractor[R, D],
    e: Extractor[R, E],
    f: Extractor[R, F],
    g: Extractor[R, G],
) -> Extractor[R, M]
into(
    make: Callable[[A, B, C, D, E, F, G, H], M],
    a: Extractor[R, A],
    b: Extractor[R, B],
    c: Extractor[R, C],
    d: Extractor[R, D],
    e: Extractor[R, E],
    f: Extractor[R, F],
    g: Extractor[R, G],
    h: Extractor[R, H],
) -> Extractor[R, M]
into(
    make: Callable[
        [A, B, C, D, E, F, G, H, J], M
    ],
    a: Extractor[R, A],
    b: Extractor[R, B],
    c: Extractor[R, C],
    d: Extractor[R, D],
    e: Extractor[R, E],
    f: Extractor[R, F],
    g: Extractor[R, G],
    h: Extractor[R, H],
    j: Extractor[R, J],
) -> Extractor[R, M]
into(
    make: Callable[
        [A, B, C, D, E, F, G, H, J, K], M
    ],
    a: Extractor[R, A],
    b: Extractor[R, B],
    c: Extractor[R, C],
    d: Extractor[R, D],
    e: Extractor[R, E],
    f: Extractor[R, F],
    g: Extractor[R, G],
    h: Extractor[R, H],
    j: Extractor[R, J],
    k: Extractor[R, K],
) -> Extractor[R, M]
into(
    make: Callable[..., M], *extractors: AnyExtractor
) -> Extractor[Never, M]

Combine several extractors into one that builds a typed value.

The escape hatch from a handler's extractor-arity ceiling, and the way to parse a group of inputs into one model: make is the model's constructor (or any factory) and each extractor supplies one positional argument to it, in order, with the types tied so a mismatch is a mypy error. The constituents' OpenAPI fragments (query/header/body) are carried through; path parameters still appear in the route pattern, so their schema comes from there.

This reuses the existing tokens rather than re-reading the request: pass the same path_param/query_param values you would otherwise hand the handler, plus the type that assembles them.

make is called positionally, which a frozen dataclass or NamedTuple constructor accepts directly. For a pydantic model (whose __init__ is keyword-only, and whose validators you want to run), pass a small factory that constructs it by keyword: into(lambda a, b: M(x=a, y=b), ea, eb). A validator that rejects raises ValidationError, which the router's exception handlers map like any other parse failure.

once

once(
    parse: Callable[[E], V],
) -> Callable[[tuple[E, ...]], V]

Adapt a single-value parse into the tuple-taking form query_param and header_param expect, requiring the value to appear exactly once.

Use it for a singleton field that must be present once: it raises ValueError when the value is absent or repeated (a duplicated singleton is a protocol violation, RFC 9110 §5.3) and otherwise applies parse to the sole value. For a genuinely list-valued field, skip this and let parse take every value.

optional

optional(
    parse: Callable[[E], V],
) -> Callable[[tuple[E, ...]], V | None]

Like once, but for a field that may appear zero or one times.

Returns None when the value is absent and parse(value) when it appears once; a repeated value still raises ValueError (a duplicated singleton is a protocol violation, RFC 9110 §5.3). Use it for an optional singleton field, and once when the field is required.

path_param

path_param(
    name: str, converter: Converter[V]
) -> Extractor[RequestHead, V]

A typed path parameter: one value that is both a pattern segment and a read.

The same converter the router matches the segment with also fixes the type V the handler receives, so there is no second place to keep in sync: drop this extractor into the route pattern (("todos", path_param("id", INT))) and into the handler's argument list, and the name, converter, schema, and type are all declared exactly once. The read casts the value the router's walk already parsed with this very converter, so the cast is sound.

query_param

query_param(
    name: str,
    parse: Callable[[tuple[str, ...]], V],
    *,
    schema: SchemaRef,
    required: bool = False,
) -> Extractor[RequestHead, V]

Parse a query parameter into V, given all of its raw values.

parse receives the (possibly empty, possibly repeated) values for name as an immutable tuple and decides what their absence and multiplicity mean, returning V or raising a ValueError to reject, which becomes an ExtractionError naming this name. The schema is this value's OpenAPI contribution.

websocket_scope

Hand a websocket handler the unparsed WebsocketScope.

The websocket sibling of http_scope(): its context is WebsocketRequestHead, so it reads the narrowed scope with no runtime check, and use on an HTTP route is a static type error.

handle

handle(
    *,
    fn: Callable[[T], Returned],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
) -> HttpEndpoint[T]
handle(
    a: Extractor[BufferedRequest, A],
    /,
    *,
    fn: Callable[[T, A], Returned],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
) -> HttpEndpoint[T]
handle(
    a: Extractor[BufferedRequest, A],
    b: Extractor[BufferedRequest, B],
    /,
    *,
    fn: Callable[[T, A, B], Returned],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
) -> HttpEndpoint[T]
handle(
    a: Extractor[BufferedRequest, A],
    b: Extractor[BufferedRequest, B],
    c: Extractor[BufferedRequest, C],
    /,
    *,
    fn: Callable[[T, A, B, C], Returned],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
) -> HttpEndpoint[T]
handle(
    a: Extractor[BufferedRequest, A],
    b: Extractor[BufferedRequest, B],
    c: Extractor[BufferedRequest, C],
    d: Extractor[BufferedRequest, D],
    /,
    *,
    fn: Callable[[T, A, B, C, D], Returned],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
) -> HttpEndpoint[T]
handle(
    a: Extractor[BufferedRequest, A],
    b: Extractor[BufferedRequest, B],
    c: Extractor[BufferedRequest, C],
    d: Extractor[BufferedRequest, D],
    e: Extractor[BufferedRequest, E],
    /,
    *,
    fn: Callable[[T, A, B, C, D, E], Returned],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
) -> HttpEndpoint[T]
handle(
    a: Extractor[BufferedRequest, A],
    b: Extractor[BufferedRequest, B],
    c: Extractor[BufferedRequest, C],
    d: Extractor[BufferedRequest, D],
    e: Extractor[BufferedRequest, E],
    f: Extractor[BufferedRequest, F],
    /,
    *,
    fn: Callable[[T, A, B, C, D, E, F], Returned],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
) -> HttpEndpoint[T]
handle(
    a: Extractor[BufferedRequest, A],
    b: Extractor[BufferedRequest, B],
    c: Extractor[BufferedRequest, C],
    d: Extractor[BufferedRequest, D],
    e: Extractor[BufferedRequest, E],
    f: Extractor[BufferedRequest, F],
    g: Extractor[BufferedRequest, G],
    /,
    *,
    fn: Callable[
        [T, A, B, C, D, E, F, G], Returned
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
) -> HttpEndpoint[T]
handle(
    a: Extractor[BufferedRequest, A],
    b: Extractor[BufferedRequest, B],
    c: Extractor[BufferedRequest, C],
    d: Extractor[BufferedRequest, D],
    e: Extractor[BufferedRequest, E],
    f: Extractor[BufferedRequest, F],
    g: Extractor[BufferedRequest, G],
    h: Extractor[BufferedRequest, H],
    /,
    *,
    fn: Callable[
        [T, A, B, C, D, E, F, G, H], Returned
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
) -> HttpEndpoint[T]
handle(
    a: Extractor[BufferedRequest, A],
    b: Extractor[BufferedRequest, B],
    c: Extractor[BufferedRequest, C],
    d: Extractor[BufferedRequest, D],
    e: Extractor[BufferedRequest, E],
    f: Extractor[BufferedRequest, F],
    g: Extractor[BufferedRequest, G],
    h: Extractor[BufferedRequest, H],
    j: Extractor[BufferedRequest, J],
    /,
    *,
    fn: Callable[
        [T, A, B, C, D, E, F, G, H, J], Returned
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
) -> HttpEndpoint[T]
handle(
    a: Extractor[BufferedRequest, A],
    b: Extractor[BufferedRequest, B],
    c: Extractor[BufferedRequest, C],
    d: Extractor[BufferedRequest, D],
    e: Extractor[BufferedRequest, E],
    f: Extractor[BufferedRequest, F],
    g: Extractor[BufferedRequest, G],
    h: Extractor[BufferedRequest, H],
    j: Extractor[BufferedRequest, J],
    k: Extractor[BufferedRequest, K],
    /,
    *,
    fn: Callable[
        [T, A, B, C, D, E, F, G, H, J, K],
        Returned,
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
) -> HttpEndpoint[T]
handle(
    *extractors: Extractor[BufferedRequest, object],
    fn: Callable[..., Returned],
    summary: str = "",
    responses: Mapping[int, ResponseSpec] | None = None,
) -> HttpEndpoint[T]

Build a self-describing endpoint from typed extractors and a handler.

Each extractor is a typed piece of the request; the overloads tie the extractors' types to fn's parameters, so a path_param(..., INT) paired with an fn that expects a str is a mypy error, not a runtime surprise.

At dispatch the input body is buffered once, a BufferedRequest is built, every extractor runs (raising to reject, mapped by the router's exception handlers), and fn is called with the typed values. The handler is always async; the output is free: an async def that resolves to a Response, or an async def ... yield that streams Outbound events, and _emit relays whichever. The endpoint also answers describe(), recovering its query/header/body OpenAPI from the same extractors.

handle is the lower-level builder; reach for the @get/@post/... method decorators to co-locate the route with its handler.

handle_stream

handle_stream(
    *,
    fn: Callable[[T, Stream[Inbound]], Returned],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
    request_body: Body | None = ...,
) -> HttpEndpoint[T]
handle_stream(
    a: Extractor[HttpRequestHead, A],
    /,
    *,
    fn: Callable[[T, A, Stream[Inbound]], Returned],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
    request_body: Body | None = ...,
) -> HttpEndpoint[T]
handle_stream(
    a: Extractor[HttpRequestHead, A],
    b: Extractor[HttpRequestHead, B],
    /,
    *,
    fn: Callable[[T, A, B, Stream[Inbound]], Returned],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
    request_body: Body | None = ...,
) -> HttpEndpoint[T]
handle_stream(
    a: Extractor[HttpRequestHead, A],
    b: Extractor[HttpRequestHead, B],
    c: Extractor[HttpRequestHead, C],
    /,
    *,
    fn: Callable[
        [T, A, B, C, Stream[Inbound]], Returned
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
    request_body: Body | None = ...,
) -> HttpEndpoint[T]
handle_stream(
    a: Extractor[HttpRequestHead, A],
    b: Extractor[HttpRequestHead, B],
    c: Extractor[HttpRequestHead, C],
    d: Extractor[HttpRequestHead, D],
    /,
    *,
    fn: Callable[
        [T, A, B, C, D, Stream[Inbound]], Returned
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
    request_body: Body | None = ...,
) -> HttpEndpoint[T]
handle_stream(
    a: Extractor[HttpRequestHead, A],
    b: Extractor[HttpRequestHead, B],
    c: Extractor[HttpRequestHead, C],
    d: Extractor[HttpRequestHead, D],
    e: Extractor[HttpRequestHead, E],
    /,
    *,
    fn: Callable[
        [T, A, B, C, D, E, Stream[Inbound]], Returned
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
    request_body: Body | None = ...,
) -> HttpEndpoint[T]
handle_stream(
    a: Extractor[HttpRequestHead, A],
    b: Extractor[HttpRequestHead, B],
    c: Extractor[HttpRequestHead, C],
    d: Extractor[HttpRequestHead, D],
    e: Extractor[HttpRequestHead, E],
    f: Extractor[HttpRequestHead, F],
    /,
    *,
    fn: Callable[
        [T, A, B, C, D, E, F, Stream[Inbound]],
        Returned,
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
    request_body: Body | None = ...,
) -> HttpEndpoint[T]
handle_stream(
    a: Extractor[HttpRequestHead, A],
    b: Extractor[HttpRequestHead, B],
    c: Extractor[HttpRequestHead, C],
    d: Extractor[HttpRequestHead, D],
    e: Extractor[HttpRequestHead, E],
    f: Extractor[HttpRequestHead, F],
    g: Extractor[HttpRequestHead, G],
    /,
    *,
    fn: Callable[
        [T, A, B, C, D, E, F, G, Stream[Inbound]],
        Returned,
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
    request_body: Body | None = ...,
) -> HttpEndpoint[T]
handle_stream(
    a: Extractor[HttpRequestHead, A],
    b: Extractor[HttpRequestHead, B],
    c: Extractor[HttpRequestHead, C],
    d: Extractor[HttpRequestHead, D],
    e: Extractor[HttpRequestHead, E],
    f: Extractor[HttpRequestHead, F],
    g: Extractor[HttpRequestHead, G],
    h: Extractor[HttpRequestHead, H],
    /,
    *,
    fn: Callable[
        [
            T,
            A,
            B,
            C,
            D,
            E,
            F,
            G,
            H,
            Stream[Inbound],
        ],
        Returned,
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
    request_body: Body | None = ...,
) -> HttpEndpoint[T]
handle_stream(
    a: Extractor[HttpRequestHead, A],
    b: Extractor[HttpRequestHead, B],
    c: Extractor[HttpRequestHead, C],
    d: Extractor[HttpRequestHead, D],
    e: Extractor[HttpRequestHead, E],
    f: Extractor[HttpRequestHead, F],
    g: Extractor[HttpRequestHead, G],
    h: Extractor[HttpRequestHead, H],
    j: Extractor[HttpRequestHead, J],
    /,
    *,
    fn: Callable[
        [
            T,
            A,
            B,
            C,
            D,
            E,
            F,
            G,
            H,
            J,
            Stream[Inbound],
        ],
        Returned,
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
    request_body: Body | None = ...,
) -> HttpEndpoint[T]
handle_stream(
    a: Extractor[HttpRequestHead, A],
    b: Extractor[HttpRequestHead, B],
    c: Extractor[HttpRequestHead, C],
    d: Extractor[HttpRequestHead, D],
    e: Extractor[HttpRequestHead, E],
    f: Extractor[HttpRequestHead, F],
    g: Extractor[HttpRequestHead, G],
    h: Extractor[HttpRequestHead, H],
    j: Extractor[HttpRequestHead, J],
    k: Extractor[HttpRequestHead, K],
    /,
    *,
    fn: Callable[
        [
            T,
            A,
            B,
            C,
            D,
            E,
            F,
            G,
            H,
            J,
            K,
            Stream[Inbound],
        ],
        Returned,
    ],
    summary: str = ...,
    responses: Mapping[int, ResponseSpec] | None = ...,
    request_body: Body | None = ...,
) -> HttpEndpoint[T]
handle_stream(
    *extractors: Extractor[HttpRequestHead, object],
    fn: Callable[..., Returned],
    summary: str = "",
    responses: Mapping[int, ResponseSpec] | None = None,
    request_body: Body | None = None,
) -> HttpEndpoint[T]

Build an endpoint whose handler reads the inbound stream live.

The streaming-input sibling of handle. Where handle buffers the request body before the handler runs (so a body extractor can read it), this leaves the inbound stream untouched and hands it to the handler as a trailing Stream[Inbound] argument: the handler is the processor, taking the state, the typed extractor values, and the live stream, reading it as events arrive (a streaming upload, a long poll, a loop driven by request chunks). The extractors are scope-only (path_param/query_param/header_param/ http_scope, whose context is the streaming route's HttpRequestHead); a body extractor is a static type error, since its BufferedRequest context is exactly the buffering a streaming route avoids. The output is free, exactly as in handle: yield Outbound to stream the response, or return (or await) a Response to buffer it.

Reach for the @get.stream/@post.stream/... method decorators to co-locate the streaming route with its handler.

ws

The websocket sibling of @get/@post, tying extractors to a handler.

@ws(t"/feed/{room}", room, since) co-locates the route with the handler and ties each extractor's type to its parameters, just like @get. The handler is the frame processor (the same move as @post.stream): it takes the live inbound frames as a trailing Stream[WebsocketInbound] argument and yields WebsocketOutbound, rather than returning a processor. There is no body to buffer: path_param, query_param, and header_param read the handshake, and a body (or http_scope) token is a static type error, its context not the WebsocketRequestHead a websocket route provides. Returns a WebsocketRoute to pass to a WebsocketRouter.

describe

Attach a RouteSpec to an endpoint, making it self-describing.

The same value the handler is built around (its body/response types) becomes its OpenAPI contribution: one declaration, two consumers. Reads as a decorator above buffered, so the endpoint stays a plain callable that also answers describe().

split_path

split_path(path: str) -> tuple[str, ...]

Split a request target into its segments.

Leading and trailing slashes are stripped, so / is the empty tuple and a trailing slash never produces an empty segment: /users and /users/ both split to ("users",). Matching is therefore trailing-slash insensitive, because targets and the literal parts of patterns are split by this same function.

buffered

buffered(
    make: Callable[[T, Match[HttpScope], bytes], Response],
) -> Endpoint[T, HttpScope, HttpHandler]

Adapt a body-reading (state, match, body) -> Response into an Endpoint.

The web-flavored sibling of without_asgi.routing.buffered: it hands the handler the Match (the scope plus the router's already-parsed path parameters) rather than the bare scope, so a handler reads match.params and match.scope without re-parsing the target. Reads the whole request body, then runs make once and emits the single Response. Usable as a decorator.

delegate

delegate(
    prefix: str, target: HttpRouter[T]
) -> Delegate[T]

Mount an opaque HTTP app at prefix (see Delegate).

mount

mount(
    prefix: str, *middleware: HttpMiddleware[object]
) -> _Mount

Bake a literal prefix (and optional per-route middleware) into HTTP routes.

Returns a transform that rebases each Route/Delegate you hand it: the prefix is prepended to the path and the middleware wrapped onto each endpoint. The result is a plain route whose segments already include the prefix, so there is no Mount wrapper in the router, matching and OpenAPI see the full path, and reverse routing (url_for) needs no router. Store and reuse it, apply it to many routes at once, or use it as a decorator on one:

api = mount("/api", require_auth)        # a reusable mount point
routes = api(list_users, create_user)    # -> a tuple of rebased routes

@mount("/api")                           # or as a decorator on one route
@get(t"/users/{uid}", uid)
async def show_user(...): ...

Nesting composes (mount("/api")(mount("/v1")(r)) -> /api/v1/...). For a sub-app whose routes you cannot see, use delegate(...) instead: an opaque app cannot have a prefix baked in, so it stays a black box handed the trimmed scope.

route

route(
    pattern: Pattern,
    *,
    get: HttpEndpoint[T] | None = None,
    head: HttpEndpoint[T] | None = None,
    post: HttpEndpoint[T] | None = None,
    put: HttpEndpoint[T] | None = None,
    patch: HttpEndpoint[T] | None = None,
    delete: HttpEndpoint[T] | None = None,
    options: HttpEndpoint[T] | None = None,
) -> Route[T]

Build a Route, one endpoint per method keyword.

url_for

url_for(
    route: Reversible,
    values: Mapping[str, object] = _NO_VALUES,
) -> str

Reverse a route to a concrete path: the inverse of the trie walk, as a pure function.

url_for(route, values) fills the route's path parameters from values and renders the path it would match at, so a handler or template links to a route by its value rather than hand-assembling a string that drifts when the path changes. Because mount bakes any prefix into the route, the route's segments are its full path: reversing needs no router, holds no hidden prefix for another router to be ignorant of, and works the same whether the route came from @get, mount(...), or a third-party package. A handler links by referencing the route value (immutable), never the assembled router.

Each value is rendered and fed back through its segment's converter to prove it would parse straight back (parse, don't validate, in reverse): a value the converter would reject, one that does not round-trip, or a single-segment value containing / raises, as does a missing or unknown parameter. A catch_all segment is the one place / is allowed.

with_middleware

with_middleware(
    endpoint: Endpoint[T, S, H],
    *middleware: Middleware[T, H, S],
) -> Endpoint[T, S, H]

Scope middleware to one endpoint instead of the whole router.

The router-wide middleware runs on every dispatch; this applies the same Middleware vocabulary to a single route (or an opaque delegate target). An Endpoint builds the handler and a Middleware is (handler, T, S) -> handler, so this is just composition: build the handler, then run the middleware over it with the request's scope. First argument is outermost, matching stack. Use it per method, e.g. route("/admin", get=with_middleware(list_admins, require_auth)); for a whole prefix, hand the middleware to mount(...).

ws_delegate

ws_delegate(
    prefix: str, target: WebsocketRouter[T]
) -> WebsocketDelegate[T]

Mount an opaque WebSocket app at prefix (see WebsocketDelegate).

ws_mount

ws_mount(
    prefix: str, *middleware: WebsocketMiddleware[object]
) -> _WsMount

The WebSocket sibling of mount: bake a prefix (and middleware) into WebSocket routes.

ws_route

ws_route(
    pattern: Pattern, endpoint: WebsocketEndpoint[T]
) -> WebsocketRoute[T]

Build a WebsocketRoute.