# maw
> A Rust async web framework built on hyper/tokio. Routing via matchit, middleware
> chains, typed request/response helpers, and optional features (cookies, sessions,
> CSRF, websockets, static files, minijinja templates).
The core flow: build a `Router`, attach handlers and middleware, hand it to an `App`,
then `listen`. Each request produces a `Ctx` that flows through the handler chain.
## Core Types
### App
The top-level server. Owns the router, optional shared state, template engine, and
global locals.
- `App::new() -> App` / `App::default()`
Creates an app with empty router, 4 MiB body limit, 10s shutdown timeout.
- `app.with_state(state: S) -> App<S>`
Attaches typed shared state. Returns a re-typed `App<S>`. Access later via
`app.state` (an `Arc<S>`).
- `app.router(router: Router) -> Self`
Sets the router. Builder-style, consumes and returns self.
- `app.body_limit(limit: usize) -> Self`
Sets the default max request body size in bytes. Default is 4 MiB. Can be
overridden per-request later (see `Request::set_body_limit` / BodyLimitMiddleware).
- `app.proxy_header(header: impl Into<String>) -> Self`
Tells the app to read the client IP from the given header (e.g. "X-Forwarded-For").
Empty string disables it. Affects `Request::ip()`.
- `app.proxy_header_fn(f) -> Self`
Same as above but the header name is chosen at request time by a closure returning
`Option<String>`. Returning `None` falls back to the socket address.
- `app.dump_routes(enable: bool) -> Self`
When true, logs the full flattened route table at startup (via tracing).
- `app.shutdown_timeout(timeout: Duration) -> Self`
How long graceful shutdown waits for in-flight connections before giving up.
Default 10s.
- `app.shutdown_token() -> CancellationToken`
Returns a clone of the cancellation token; cancel it to trigger shutdown.
- `app.locals(f: FnOnce(&AnyMap)) -> &Self`
Read access to global locals under a read lock.
- `app.locals_mut(f: FnOnce(&mut AnyMap)) -> &Self`
Mutable access to global locals under a write lock.
- `app.with_locals(f) -> Self`
Builder-style mutable locals access; consumes and returns self.
- `app.views(path) -> Self` [feature: minijinja]
Sets the template directory for the minijinja engine.
- `app.views_with(path, f) -> Self` [feature: minijinja]
Sets the template directory and runs a closure to configure the Environment
(register filters, globals, etc).
- `app.listen(addr).await -> Result<(), Error>`
Binds and serves. Installs a Ctrl+C handler that triggers graceful shutdown.
Builds the router, runs middleware lifecycle hooks, then accepts connections.
- `app.listen_shutdown(addr, shutdown: CancellationToken).await -> Result<(), Error>`
Same as `listen` but you supply the shutdown token instead of relying on Ctrl+C.
Notes on listen behavior:
- Builds the matchit router from the route tree; route conflicts panic at build.
- Collects every unique middleware (by type id) and calls `on_app_listen_mut`
then `on_app_listen_arc` on each, giving middleware a chance to register
globals or mutate the app before it's frozen in an Arc.
- With the `listenfd` feature, will reuse a passed-in socket fd if present.
### Router
Defines routes and middleware. Cheaply cloneable (shares an `Arc<Mutex<Vec<...>>>`
of items internally). Methods return a clone so calls can be chained.
- `Router::new() -> Router`
Root router with empty path.
- `Router::group(path) -> Router`
Creates a sub-router rooted at `path`. Panics unless the path starts with `/` and
does not end with `/` (except the literal "/").
- `router.push(child: Router) -> Self`
Nests a child router. Used internally by route registration but public.
- HTTP method registration (all take `path` and `handlers`):
`router.get(path, handlers)`
`router.post(path, handlers)`
`router.put(path, handlers)`
`router.delete(path, handlers)`
`router.head(path, handlers)`
`router.options(path, handlers)`
`router.connect(path, handlers)`
`router.patch(path, handlers)`
`router.trace(path, handlers)`
`router.add(method, path, handlers)` // explicit method
`router.all(path, handlers)` // matches any method as a fallback
`handlers` may be a single handler, or a tuple `(mw1, mw2, ..., final_handler)`
where all but the last are treated as middleware scoped to that route. Tuples up
to 10 elements are supported.
- `router.middleware(handlers) -> Self`
Registers middleware on this router. All routes defined after it on the same
router (and its children) inherit it. A tuple registers several middleware in
order.
- `router.ws(path, handler) -> Self` [feature: websocket]
Registers a GET route that upgrades to a websocket. `handler: Fn(WebSocket) -> Future`.
- `router.static_files(prefix, files) -> Self` [feature: static_files]
Serves embedded files under `prefix`. Registers an index route (if the index
exists) plus a catch-all.
Routing/matching behavior at request time:
- Paths are normalized: backslashes become forward slashes, a trailing slash is
stripped (except root).
- Method lookup falls back: HEAD falls back to the GET handler; if no method
matches, the `all()` handler is tried.
- No path match -> 404. Path matches but no method -> 405.
- For HEAD requests the body is stripped after the handler runs.
- Path params come from matchit and land in `req.params`.
### Ctx
The per-request context threaded through the handler chain. Holds the request,
response, and the handler chain itself.
- `c.req: Request` — the incoming request (public field)
- `c.res: Response` — the outgoing response (public field)
- `c.cookies` — CookieStore [feature: middleware-cookie]
- `c.session` — SessionStore [feature: middleware-session]
- `c.next().await`
Invokes the next handler in the chain. Middleware call this to continue; not
calling it short-circuits the chain. Does nothing if the context is closed.
- `c.app() -> &App`
The app this request belongs to.
- `c.is_closed() -> bool` / `c.close()`
`close()` marks the context closed so the chain stops and the response is
suppressed (the server returns no response body for that request).
- `c.handlers() -> &[DynHandlerRun]` — the full chain for this route.
- `c.current_handler_index() -> usize` — position in the chain.
- `c.is_websocket() -> bool` [feature: websocket]
- `c.upgrade_websocket(handler) -> Result<(), WsUpgradeError>` [feature: websocket]
Upgrades the connection and spawns `handler(WebSocket)` on a tokio task. Errors
if the request isn't a websocket upgrade.
- `c.csrf_token() -> &str` [feature: middleware-csrf]
The token for the current request (empty if unset).
- `Ctx::csrf_header() -> &'static str` [feature: middleware-csrf]
Returns "X-CSRF-Token".
## Request
Accessed as `c.req`. Most body methods are async and cache the body after first read.
Path / metadata:
- `req.param::<T>(key) -> Result<T, ParamError>`
Path param parsed into `T` (via serde_plain). Errors if missing or unparsable.
- `req.param_str(key) -> &str`
Raw path param, or "" if absent.
- `req.method() -> &Method`
- `req.uri() -> &Uri`
- `req.version() -> Version`
- `req.headers() -> &HeaderMap` / `req.headers_mut() -> &mut HeaderMap`
- `req.header(key) -> Option<&str>`
Single header as a string; `None` if absent or not valid UTF-8.
- `req.app() -> &App`
Client info:
- `req.ip() -> String`
Honors the configured proxy header if set, else the socket address.
- `req.is_local() -> bool`
True if the peer address is loopback.
Body:
- `req.set_body_limit(limit)`
Per-request override of the max body size.
- `req.take_body() -> Option<IncomingBody>`
Takes the raw streaming body (consumes it; later body reads will fail).
- `req.body().await -> Result<&Bytes, BodyError>`
Reads and caches the full body, enforcing the body limit. Subsequent calls
return the cached bytes.
- `req.text().await -> Result<&str, BodyError>`
Body as UTF-8.
- `req.json::<T>().await -> Result<T, ParseError>`
Deserialize body as JSON.
- `req.form::<T>().await -> Result<T, ParseError>`
Deserialize body as URL-encoded form.
- `req.xml::<T>().await -> Result<T, ParseError>` [feature: xml]
- `req.parse::<T>().await -> Result<T, ParseError>`
Dispatches on Content-Type: JSON, x-www-form-urlencoded, or XML (if enabled).
Errors with UnsupportedMediaType / MissingContentType otherwise.
- `req.content_type() -> Option<Mime>`
- `req.size_hint() -> SizeHint`
- `req.multipart() -> Result<Multipart, MultipartError>`
Builds a multipart reader from the body. Takes the body. Errors if not
multipart/form-data or boundary is missing.
Query string:
- `req.query::<T>() -> Result<T, QueryError>`
Whole query string deserialized into `T`.
- `req.query_value::<T>(key) -> Result<T, QueryError>`
A single query param parsed into `T`. Errors if missing or unparsable.
Locals:
- `req.locals` — an `AnyMap` for stashing per-request data (e.g. middleware writing
values for downstream handlers). Values must be `Clone + Send + Sync`.
## Response
Accessed as `c.res`. Setter methods generally return `&mut Self` for chaining; the
`send*` family set the body and return `()`.
Status:
- `res.status(code) -> &mut Self`
Sets the status; marks status as modified.
- `res.send_status(code) -> &mut Self`
Sets status and, if the body is empty, fills it with the canonical reason phrase.
Headers:
- `res.headers() -> &HeaderMap` / `res.headers_mut() -> &mut HeaderMap`
- `res.header(headers) -> &mut Self`
Sets (inserts) one or more headers. Accepts a `(key, value)` tuple, a fixed array,
or a `Vec` of tuples. Conversion failures are logged, not returned.
- `res.append(key, values) -> &mut Self`
Appends header value(s) without replacing existing ones. Values can be a single
string/HeaderValue or a collection.
- `res.content_type(value) -> &mut Self`
Body:
- `res.send(body: impl Into<Bytes>)`
Sets the body to a full (non-streaming) payload.
- `res.html(s: &'static str)`
Sets `text/html; charset=utf-8` and sends `s`.
- `res.json(value: impl Serialize)`
Serializes to JSON, sets `application/json`. On serialize failure responds 500.
- `res.stream(stream)`
Streams a body from a `Stream<Item = Result<Bytes, E>>`.
- `res.stream_frames(stream)`
Streams http_body Frames (data + trailers).
- `res.sse(stream)`
Server-sent events: sets event-stream headers, disables proxy buffering, and ties
the stream's lifetime to the app shutdown token (stops streaming on shutdown).
- `res.send_file(path).await -> Result<(), io::Error>`
Streams a file from disk with guessed Content-Type and Content-Length (64 KiB
chunks).
Redirects:
- `res.redirect(location, status: Option<StatusCode>)`
Sets Location and status (defaults to 302 Found).
Templates [feature: minijinja]:
- `res.render(template)` — render a named template with the locals context.
- `res.render_with(template, value)` — same, plus extra context merged in.
- `res.render_str(source)` — render an inline template string.
- `res.render_str_with(source, value)`
- `res.get_render_ctx() -> minijinja::Value`
Builds the context from app locals + response locals.
On render error these respond 500 and log.
Locals:
- `res.locals` — an `AnyMap` of `Serialize`-able values, used as template context and
for passing data along the response.
## Handlers
A handler is any `Fn(&mut Ctx) -> impl Future`. The return value's type determines the
response via `IntoResponse`:
- `()` — does nothing (handler wrote to `c.res` directly).
- `String`, `&'static str`, `Vec<u8>`, `&'static [u8]`, `Bytes`,
`Cow<'static, str/[u8]>` — sent as the body.
- `StatusCode` — sets the status (with canonical body).
- `StatusError` — sets that error's status + brief body.
- `Option<T>` — sends `T` if Some, otherwise leaves the response untouched.
- `Result<T, StatusError>` — Ok sends `T`, Err sends the error.
- `HttpBody` — replaces the response body directly.
Handlers carrying state:
- `WithState(state, f)` where `f: Fn(&mut Ctx, S) -> Future`
Wraps a handler with a cloned piece of state passed in each call. The state is
also retrievable from the type-erased handler via `get_state`.
## StatusError
A rich HTTP error carrying code, name, brief, optional detail, and an optional source
error. Constructors exist for every standard status (e.g. `StatusError::not_found()`,
`bad_request()`, `unauthorized()`, `internal_server_error()`, ... through the 4xx/5xx
range).
- `.brief(msg) -> Self` — override the short message.
- `.detail(msg) -> Self` — attach extra detail.
- `.error(err) -> Self` — attach a source error.
- `.detailed_display() -> String` — full multi-field rendering.
- `StatusError::from_code(code) -> Option<Self>` — build from a StatusCode.
It implements `std::error::Error` and `IntoResponse`, and the request/cookie/session
error types all convert into it, so handlers can `?` them into responses.
## Middleware
Each is registered via `router.middleware(...)`. Middleware are handlers that usually
call `c.next().await` to continue the chain.
- `LoggingMiddleware::new()` [feature: middleware-logging]
Logs status, elapsed time (human-formatted), client IP, method, and path after the
request completes.
- `CatchPanicMiddleware::new()` [feature: middleware-catch_panic]
Catches panics in downstream handlers and responds 500.
`.on_panic(f)` installs a custom handler `Fn(&mut Ctx, Box<dyn Any + Send>) -> Future`
to handle the panic instead.
- `BodyLimitMiddleware::new(max)` [feature: middleware-body_limit]
Sets the per-request body limit for downstream handlers.
- `CookieMiddleware::new()` [feature: middleware-cookie]
Parses the Cookie header into `c.cookies` before the chain and writes any added/
changed cookies as Set-Cookie headers after.
`.key(key)` supplies a signing/encryption key (accepts a `cookie::Key`, byte slice,
Vec, or string).
- `SessionMiddleware::new()` [feature: middleware-session]
Loads the session into `c.session` and persists it if modified.
`.storage(impl SessionStorage)` chooses the backend (default is cookie-inline storage).
`.cookie_name(name)`, `.cookie_type(type)`, `.cookie_options(opts)` configure the
session cookie. Plain cookie type is rejected for security.
- `CsrfMiddleware::new()` / `default()` [feature: middleware-csrf]
Issues and validates CSRF tokens. Safe methods (GET/HEAD/OPTIONS/TRACE by default)
are exempt; others must present a matching `X-CSRF-Token` header or get 403.
`.storage(CsrfStorage::Cookie | Session)`, `.key_name(name)`, `.safe_methods(vec)`,
`.cookie_type(type)`, `.cookie_options(opts)`. Comparison is constant-time. Also
registers a `csrf_header` global in templates.
## CookieStore (c.cookies) [feature: middleware-cookie]
Per-request cookie jar. Values are serialized with postcard and base64-encoded.
- `cookies.get::<T>(name) -> Result<T, CookieError>` — plain.
- `cookies.get_signed::<T>(name)` / `get_encrypted::<T>(name)` — require a key.
- `cookies.get_typed::<T>(name, &CookieType)` — dispatch on type.
- `cookies.set(name, &value, options)` / `set_signed` / `set_encrypted` / `set_typed`
Options is `Option<CookieOptions>`.
- `cookies.remove(name)`
`CookieType`: `Plain | Signed | Encrypted`.
`CookieOptions::new()` with builders: `.path`, `.domain`, `.secure(bool)`,
`.secure_fn(closure)`, `.http_only`, `.same_site`, `.max_age`, `.expires`.
## SessionStore (c.session) [feature: middleware-session]
A key/value store serialized with postcard.
- `session.get::<T>(key) -> Result<T, SessionError>`
- `session.set(key, value)` — marks modified.
- `session.remove(key) -> bool` — marks modified.
- `session.clear()` — marks modified.
- `session.contains_key(key) -> bool`
- `session.keys() -> impl Iterator`
- `session.is_modified() -> bool`
Custom backends implement `SessionStorage` (`INLINE`, async `load`/`save`, and a
provided `generate_id`). The default `CookieStorage` is inline (stores the whole
session in the cookie).
## WebSocket [feature: websocket]
Obtained inside the closure passed to `router.ws(...)` or `c.upgrade_websocket(...)`.
- `ws.recv().await -> Option<Result<Message, WsError>>`
- `ws.send(msg).await -> Result<(), WsError>`
- `ws.close(frame).await -> Result<(), WsError>`
Also implements `Stream` and `Sink<Message>`.
- `req.is_websocket() -> bool` — true if the Upgrade header requests a websocket.
## StaticFiles [feature: static_files]
Wraps a `rust_embed::RustEmbed` type for serving embedded assets.
- `StaticFiles::new(embed)`
- `.index(file)` — index filename (default "index.html").
- `.max_age(seconds)` — sets Cache-Control max-age (0 disables).
- `.fallback_to(file)` — file to serve when a path isn't found (e.g. SPA index).
Serving behavior: resolves index for directory-like paths, honors If-Modified-Since
(304 when unchanged), sets Last-Modified, guesses Content-Type, applies cache-control,
and 404s when nothing matches and no fallback is set.
## AnyMap
A type-erased string-keyed map used for `locals`. Two flavors: `CloneableAny`
(request locals) and `SerializableAny` (response/app locals, usable as template
context). Values must satisfy `Clone + Send + Sync` (plus `Serialize` for the
serializable flavor).
- `get::<T>(key) -> Option<&T>` / `get_mut::<T>(key)`
- `insert(key, val) -> Option<T>` (returns the previous value) / `set(key, val)`
- `get_or_insert_with(key, f)` / `get_or_insert(key, val)` / `get_or_insert_default(key)`
- `remove::<T>(key) -> Option<T>`
- `contains_key`, `len`, `is_empty`, `clear`, `extend`
- `iter`, `iter_mut`, `keys`, `values`, `values_mut`, and IntoIterator support.
## Error
`Error` is the framework's startup/IO error (IO, route insert conflicts, address
parse failure, invalid header name/value). Returned from `listen`/`listen_shutdown`.
Per-request errors are the more specific types (`ParamError`, `BodyError`,
`ParseError`, `QueryError`, `MultipartError`, `CookieError`, `SessionError`,
`WsUpgradeError`), each convertible into `StatusError`.