Menu
Public repository / master
AkurAI-Framework / site/content/docs/middleware.md
Latest change 1f2c9f9983d36d42b1f177ad7bcea92a8fe8dc4d - feat: built-in server access log, default on by Ólafur Búi Ólafsson
# Middleware
Middleware wraps every request: it can inspect the request, call the next layer,
and post-process the response — for cross-cutting concerns like security
headers, logging, timing, redirects, and auth gates.
## The model
A middleware receives the request and a `next` function. It may call `next` to
continue down the chain, or short-circuit by returning a response itself:
```rust
fn handle(&self, req: &Request, next: &dyn Fn(&Request) -> Reply) -> Reply;
```
Layers compose into a `MiddlewareStack` and run **outermost-first** — the first
one pushed is the outermost, so it sees the request first and the response last.
## Running with a stack
The stack is additive: `Server::run(handler)` is unchanged, and
`Server::run_with(stack, handler)` adds the chain.
```rust
let stack = MiddlewareStack::new()
.push(SecurityHeaders) // outermost: stamps even short-circuited replies
.push(Timing); // adds X-Response-Time-Us
server.run_with(stack, handler)?;
```
Order matters: put `SecurityHeaders` outside any blocklist or auth gate so even a
rejected request gets hardened headers.
## Built-ins
- **`SecurityHeaders`** — `X-Content-Type-Options: nosniff`,
`X-Frame-Options: DENY`, `Referrer-Policy: no-referrer`.
- **`Timing`** — adds an `X-Response-Time-Us` response header.
These are active on the framework's own site — every response carries the
security and timing headers (try `curl -I` against any page).
## Access logging
Request logging is no longer a middleware — the server itself writes one access
line per request to stderr (journald-friendly under systemd), **on by
default** for every framework app:
```
127.0.0.1:44428 GET /docs/middleware?x=1 -> 200 8412B 1.2ms
127.0.0.1:46930 <malformed> -> 400 15B 8us
```
Each line carries the peer address, method and target (query included), final
status, body bytes, and wall-clock duration. Requests whose head fails to parse
are logged as `<malformed>` with the `400` the server returned — something no
middleware could see. Handlers can also read the client address from
`req.peer`. Opt out with `Server::bind(addr)?.access_log(false)`.
## Writing your own
Any closure `Fn(&Request, &dyn Fn(&Request) -> Reply) -> Reply` is a middleware,
so a one-off layer needs no new type:
```rust
stack.push(|req: &Request, next: &dyn Fn(&Request) -> Reply| {
if req.path == "/blocked" {
return Reply::Response(Response::new(403).with_text("nope"));
}
next(req)
})
```
WebSocket and SSE upgrade replies pass through untouched.