mini-serve 0.7.0

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

An HTTP server: trie router, middleware, CORS, optional TLS. Built on `hyper` + `tokio`,
nothing else required.

> Status: **planned**. Previously implemented and published as `small-serve`; being
> rebuilt from this spec with a full security/correctness pass folded in — this crate
> carried the largest share of findings from the prior iteration's review. See
> `DEV_PLAN.md`.

```toml
[dependencies]
mini-serve = { version = "0.1", features = ["err", "log", "tls"] }
```

## Design

An `App<S>` holds shared state (`Arc<S>`), a route trie, and middleware. Routes are
registered per method; path params are typed extractors, not stringly-typed lookups.

## Connection lifecycle (fixed from the prior iteration)

Three separate DoS classes existed in the connection-accept path; all three are closed
by construction now, not by configuration a caller has to remember to set:

- **TLS handshake timeout.** `header_read_timeout` only started *after* a completed TLS
  handshake — a client that opens TCP and stalls mid-handshake held its connection-
  semaphore permit forever. Fixed: the handshake itself runs under a bounded
  `tokio::time::timeout` (default 10s); a permit is never held past it.
- **`accept()` error handling doesn't busy-loop.** `EMFILE`/`ENFILE` (fd exhaustion) made
  `accept()` fail immediately and repeatedly — an unbounded retry loop that turned a
  transient resource issue into 100% CPU. Fixed: every accept error is followed by a
  short bounded sleep before retrying, selected against the shutdown signal so it never
  delays a clean shutdown by more than that sleep.
- **Ephemeral binds are loopback-only.** `bind_ephemeral`/`bind_tls_ephemeral` bind
  `127.0.0.1:0`, not `0.0.0.0:0` — a test helper should not expose a real server to the
  LAN. Callers who want an explicit external bind use `bind()`/`run()` with a real
  address; that is a decision made at the call site, not a side effect of "ephemeral."
- **Graceful shutdown cannot be blocked by a saturated semaphore.** The prior
  implementation acquired the connection-limit permit *inside* the `accept()` branch of
  the shutdown `select!`, so once every permit was taken the shutdown branch could never
  win the race — SIGTERM was ignored indefinitely under exactly the load conditions where
  a restart is most needed. Fixed: permit acquisition is itself a `select!` arm, racing
  fairly against the shutdown signal.

## CORS (fixed)

The credentialed-wildcard-reflection branch (`allow-credentials: true` combined with
reflecting any request `Origin`) — the classic CORS bypass that grants every website
scripted access to authenticated responses — does not exist in this implementation.
`CorsConfigBuilder::build()` returns `Result<CorsConfig, CorsConfigError>` and rejects
`allow_all_origins + credentials` at construction time, identically in debug and release
builds. There is no configuration that can express the unsafe combination; there is
nothing to "remember not to do."

## Error responses (fixed)

5xx responses never echo the handler's internal error message to the client — `internal
server error` is the wire body; the real message goes to the log (if the `log` feature
is enabled) or stderr. 4xx messages, which are authored for the client, pass through
unchanged. Full control remains available via `with_error_handler` for apps that want it.

## HEAD requests (fixed)

A HEAD response carries the *same* `Content-Length` a GET response to the same route
would carry — RFC 9110 §9.3.2. The prior implementation zeroed it, breaking any client
(`curl -I`, a download manager, a CDN) that uses HEAD to learn a resource's size.

## Routing (fixed + faster)

Percent-encoded path segments are decoded before matching (`GET /api/%69tems` matches a
route registered as `/api/items`); `+` in query strings decodes as space, matching
`application/x-www-form-urlencoded` semantics; a 405 response carries the `Allow` header
listing the methods that *do* match, per RFC 9110; a CORS preflight for a path with no
registered route falls through to 404 rather than masking it with a 204.

Routing is also a single trie traversal per request instead of the prior three (once for
existence, once for the match, once more for HEAD fallback) — method dispatch, 405
detection, the `Allow` header, and HEAD-via-GET all read off the one node the traversal
finds. Static-segment matching no longer clones the path-param map at every node it
merely passes through; only an actual param match copies anything.

## State sharing (fixed)

Application state is shared via the existing `Arc<S>` refcount bump per request, not
deep-cloned and re-wrapped in a fresh `Arc` — the prior implementation paid a real
allocation (and, for any state holding a `HashMap` or connection pool, a real copy) on
every single request for no reason the API required.

## Path-param extraction (simplified)

Typed path-param extraction deserializes directly from the matched segment map via
serde's `MapDeserializer` — no round trip through a synthetic, percent-encoded query
string and a second parser crate. One fewer dependency, one fewer allocation per typed
extraction.

## Example

See `examples/mini-serve.rs` for a minimal working application:

```bash
# Run locally
cargo run --example mini-serve -p mini-serve

# Run in Docker
docker compose up
```

The example wires up a `/health` endpoint (used by the container's `HEALTHCHECK`) and
a `/hello/:name` endpoint that demonstrates typed path-param extraction and JSON
response building. It binds to `0.0.0.0:$PORT` (default 8080) and responds to SIGINT
and SIGTERM for graceful shutdown.

## Features

- `err``mini_err::Error` → HTTP response conversion.
- `log` — request/response logging middleware.
- `tls``rustls`-backed TLS listener.

## Non-goals

- No built-in templating, no ORM, no background job runner — this is a router and a
  connection lifecycle, nothing else.
- No HTTP/2 server push (deprecated by browsers; not worth the surface area).