Expand description
§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. SeeDEV_PLAN.md.
[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_timeoutonly 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 boundedtokio::time::timeout(default 10s); a permit is never held past it. accept()error handling doesn’t busy-loop.EMFILE/ENFILE(fd exhaustion) madeaccept()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_ephemeralbind127.0.0.1:0, not0.0.0.0:0— a test helper should not expose a real server to the LAN. Callers who want an explicit external bind usebind()/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 shutdownselect!, 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 aselect!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:
# Run locally
cargo run --example mini-serve -p mini-serve
# Run in Docker
docker compose upThe 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).
Structs§
- App
- An HTTP server application with typed state, routing, and TLS support.
- Body
Error - An error that can occur while streaming a response body.
- Cors
Config - Cross-Origin Resource Sharing (CORS) configuration for an HTTP server.
- Cors
Config Builder - Builder for constructing a valid
CorsConfig. - MaxBody
Size - Maximum body size limit for a request.
- Path
Params - Extracted path parameters from a matched route.
- Query
Params - Parsed query parameters from the request URL.
- Route
Builder - Builder for configuring routes and settings before creating an
App. - Serve
Error - An HTTP error returned by a handler.
- State
- Shared application state passed to every request handler.
Enums§
- Cors
Config Error - Error type for CORS configuration validation.
Constants§
- DEFAULT_
MAX_ BODY_ SIZE - Default maximum request body size: 2 MiB.
Functions§
- body
- Create a response body from raw bytes.
- handler
- Wrap an async function to create a handler.
- json
- Build a JSON response with the given status code and serializable value.
- json_
body - Extract a JSON-deserialized body from the request.
- path_
params - Extract path parameters from the request and deserialize into type
T.
Type Aliases§
- Handler
- A request handler that processes an HTTP request and returns a response or error.
- Response
Body - The response body type used by all handlers.