mini-serve 0.13.8

An HTTP server: trie router, middleware, CORS, optional TLS. Built on hyper + tokio.
Documentation
# Principles

What this crate optimises for, and the rules it is held to. Written down because the two
at the end are judgement calls, and unwritten judgement calls drift: "a bit slower"
becomes "half the speed" one commit at a time, and "only where it buys security" becomes
"it seemed worth it at the time".

## What it optimises for

**One thing well.** An HTTP server: a router and a connection lifecycle. Authentication,
rate limiting, static files, certificate management and templating are all deliberately
somebody else's job — see the non-goals table in [`THREAT_MODEL.md`](THREAT_MODEL.md),
which names what to compose instead of each.

**Secure by default, not by assembly.** A connection ceiling, a per-message header
timeout, body limits, CORS applied to every exit path and `nosniff` on every response are
on without configuration. The alternative — a smaller core plus five crates you must
remember to add — optimises for the benchmark and against the person deploying it at
3 a.m.

**Auditable.** The point is not that this crate is trustworthy; it is that you can check.
Three properties make that practical, and each is verifiable in an afternoon:

- **33 dependencies** in the default tree, against axum's 53 — before an axum application
  adds `tower-http` for the CORS this ships built in. Measured 2026-08-15 with
  `cargo tree --edges normal --prefix none | awk '{print $1}' | sort -u | wc -l`.
- **No `unsafe` in this crate's own code**, enforced by `#![forbid(unsafe_code)]`, which
  unlike `deny` cannot be switched off by an inner `allow`. Dependencies including
  `tokio`, `hyper`, `bytes` and `mio` all contain `unsafe`, as any async runtime must;
  this says nothing about them.
- **Every security guarantee has a test proven to fail without it.** `THREAT_MODEL.md`
  lists them; `./verify-guarantees.sh` removes each guarantee in turn and asserts its test
  catches the removal. It is evidence, not a claim — run it.

**Composable, through named seams.** This crate is a connection lifecycle with four
places to plug in, and the invariants hold across all of them:

| Seam | What plugs in | Example |
|---|---|---|
| Handler | Application code | your routes |
| Middleware | Per-request policy | `mini-guard` — scoring, rate limiting, IP reputation |
| Fallback | Unmatched requests | `mini-static` via `mini-unified` |
| Transport | What a connection speaks before HTTP | `mini-tls` |
| Upgraded connection | A protocol that is not HTTP | a WebSocket crate |

An extension gets the guarantees for free: whatever plugs into a seam is still inside the
connection ceiling and still bounded by the shutdown drain.

`mini-tls` is the worked example. It is around forty lines, depends on neither
`mini-serve` nor anything of its shape, and bounds nothing itself — a stalled handshake is
caught by the server's `with_connect_timeout`, and a negotiating connection holds its
permit because `connect` runs inside the connection's own task. Its threat model says so,
listing what it inherits rather than restating it. Moving TLS out also took this crate from
two build configurations to one: `--all-features` and the default are now the same 33
crates.

## The performance budget

**Within 25% of best-in-class retired instructions for the same work**, where
best-in-class means *the fastest routed framework measured in the same run, on that
route* — not whichever comparison happens to flatter us. Actix is fastest on a static
route and slowest on a param route; axum is the reverse.

Measured by `bench/perf-stat.sh`, which states this as its pass criterion. Instructions
rather than wall-clock because roughly 45% of a request's CPU here is kernel time every
server pays equally, which dilutes a userspace difference by 3x and makes timing unable to
resolve it.

**Current position: +14.5% on `/health`** (15,820 against axum's 13,820) and **+18.1% on
`/hello/:name`** (18,515 against 15,706), measured 2026-08-16 on x86_64 (i5-8350U) after
`PLAN-unconditional.md`.

That figure has now moved twice, in both directions, which is the argument for the budget
existing at all:

- It was +20.6% / +21.8% on 2026-08-15.
- Security work then added ~455 instructions per request — the RFC 9112 `Host` check, the
  response funnel and the header work — taking it to **+24.0% / +24.6%**, within a
  rounding error of the 25% ceiling. Nobody noticed until the next plan measured its own
  starting point, because nothing was watching.
- `PLAN-unconditional.md` recovered ~1,300 and ~1,060, bringing it back to the figures
  above.

Two lessons, both worth keeping: the budget is a live constraint and was nearly spent by
work that was individually justified, and **a starting point must be measured, not
inherited from the last plan's ending figure.** Exceeding the budget is a bug to file, not
a fact to absorb.

Being second is acceptable. Being second by an amount nobody can measure is the goal.

## The compromise rule

**A cost is kept when it buys a stated guarantee, and that guarantee goes in the threat
model. A cost that buys nothing gets removed.** Nothing is kept because it seemed
reasonable at the time.

This has teeth in both directions, and both have been exercised:

- An allocation-reduction series removed 15–28% of per-request allocations and produced no
  measurable improvement. It was breaking API for no gain, so it was reverted — five
  commits and three version bumps discarded. `PLAN-allocations.md` records why.
- `PLAN-unconditional.md` deliberately leaves **393 instructions per request** on the
  table, every one of them buying a guarantee that is written down and mutation-tested:
  the per-message header timeout (~147, the slowloris bound), the RFC 9112 `Host` check
  (141, which hyper does not enforce), and the response funnel (~105, because a policy
  header present on 200s and missing on error responses is worse than none). All three are
  in the threat model, so they are costs someone can argue with rather than costs nobody
  can find.

The rule exists because "make it faster" and "make it safer" pull in opposite directions
often enough that a tiebreaker has to be decided in advance, in the calm, rather than
per-commit while looking at a number you want to improve.