maw 0.30.4

A simple and efficient web framework for Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# 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`.