# PATCH: benchmarking + performance/security hardening
Tracking document for a multi-phase effort to (1) stand up real benchmarking for
`mini-static` and (2) push it toward "best in class" speed and security. Check items
off as commits land. Design rationale lives in the plan this was generated from — this
file is just the live checklist.
Current version: `0.13.0`. Versioning policy for this work: patch bumps for
bench/test/doc-only commits; a minor bump (`0.14.0`) on the commit where `206`/`416`
Range support first becomes reachable via `handle_request` (a genuine behavior change).
No commits will be made automatically — the user commits manually.
---
## Phase A — Benchmark infrastructure
- [ ] Add `criterion` (`default-features = false`) as a dev-dependency; `[[bench]]`
entries with `harness = false`.
- [ ] `benches/path_resolution.rs` — `resolve_with_canonical_root`/`resolve` across
shallow/deep/percent-decoded/traversal-rejected/directory-index paths.
- [ ] `benches/streaming.rs` — `Server::handle_request` throughput (`Throughput::Bytes`)
across fixture file sizes.
- [ ] `benches/minify_cache.rs` — cache-miss vs. cache-hit cost via `with_minify()`.
- [ ] `benches/fixtures.rs` — shared fixture builder (`tempfile::TempDir`, generated at
run time, not committed): small (1KB/4KB ×20), medium (256KB/1MB ×5), large
(10MB/100MB ×2), many-small-files (500 × ~2KB / 10 subdirs), precompressed
`.br`/`.gz` sidecars (shell out to system `gzip`/`brotli`, degrade to gzip-only if
`brotli` binary absent), fingerprinted filenames for immutable-cache-control.
- [ ] `examples/serve_fixture.rs` — binary serving the fixture tree via `Server::run_*`,
driven by CLI flags (`--root`, `--port`).
- [ ] `benches/load/run.sh` — `oha`-driven load test matrix (small/large/precompressed/
many-small-files × concurrency 10/50/200 × 10s), JSON output to
`benches/load/results/<timestamp>/`.
- [ ] `benches/load/README.md` — setup + result format docs.
- [ ] `benches/load/comparison/` — `nginx.conf`, `static-web-server.toml`,
`run_comparison.sh` (sequential runs, local binaries not Docker, same fixture dir
for all three servers, environment fingerprint recorded per result). Manual/local
only — never wired into `cargo ci`.
## Phase B — Range / If-Range support (206 Partial Content)
Scope: single-range only. A `Range` header with a comma (multi-range) is treated as
absent and falls through to full 200 (RFC 9110-legal, matches common server behavior).
- [ ] Hoist `file_size` computation earlier in `handle_request` (`src/server.rs`) so
range validation has it available before the body-construction branch.
- [ ] Add `parse_range_header(header: &str, file_size: u64) -> RangeOutcome` (enum:
`NoRange | Satisfiable(u64, u64) | Unsatisfiable | MultiRangeIgnored`) next to
`is_etag_match`/`generate_etag`.
- [ ] `If-Range` validation: exact strong ETag match only (not `is_etag_match`'s weak
comparison) — stale `If-Range` ignores `Range`, serves full 200.
- [ ] `416 Range Not Satisfiable` + `Content-Range: bytes */<file_size>` for
out-of-bounds/unsatisfiable ranges.
- [ ] `206 Partial Content` + `Content-Range` + adjusted `Content-Length` for
satisfiable ranges; clamp `end` to `file_size - 1` if it overshoots.
- [ ] `Accept-Ranges: bytes` on every 200/304/206 response.
- [ ] `FileBody::new_ranged(file, start, len)` in `src/handler.rs` — `remaining:
Option<u64>` field capping `poll_frame` reads; seek via
`file.seek(SeekFrom::Start(start)).await` in `handle_request` before construction
(needs `tokio::io::AsyncSeekExt` import).
- [ ] Range + precompressed-sidecar interaction: skip sidecar, serve range from original
file when `Range` is satisfiable (mirrors existing `html_injection` bypass).
- [ ] `HEAD + Range` → same `206`/`Content-Range`/`Content-Length` headers, empty body.
- [ ] `tests/range.rs` — satisfiable/suffix/open-ended ranges, out-of-bounds → 416,
clamped end, If-Range match/mismatch, multi-range → 200, range+sidecar →
uncompressed, HEAD+Range, Accept-Ranges on plain 200.
- [ ] Inline unit tests for `parse_range_header` pure edge cases.
- [ ] Update README's Range-limitation paragraph + `DEV_PLAN.md`'s Phase 0.2.0 entry.
- [ ] **Version bump to 0.14.0** on the commit where 206/416 first becomes reachable.
## Phase C — Security hardening
- [ ] Document `Server::with_max_connections`/header-timeout as connection-level
protection only; point consumers wanting request-level throttling/blocking at
`mini-guard` (implemented — `Policy`/`decide()`/`GuardMiddleware`, wired via
`mini-unified`). No new rate-limiting code in mini-static itself.
- [ ] README "Security headers" section: keep `X-Content-Type-Options: nosniff`
unconditionally; document HSTS/CSP/X-Frame-Options as the embedder's
responsibility (context-dependent, not mini-static's to default) with guidance on
where to add them via mini-serve/mini-unified middleware.
- [ ] Add `proptest` as a dev-dependency.
- [ ] `tests/resolve_property.rs` — arbitrary path-like strings (`..`, null bytes,
percent-encoding, deep nesting, mixed slashes) asserting: any `Ok` result has
`root_canon` as a prefix.
- [ ] Extract pure helper from `read_header_prefix`'s `\r\n\r\n`-scan logic (e.g.
`contains_header_terminator(buf: &[u8], newly_read: usize) -> bool`) so it's
property-testable without a live socket per case.
- [ ] Property test for the extracted helper + one full-socket proptest confirming the
end-to-end `MAX_HEADER_BYTES` bound holds under generated inputs.
- [ ] Set explicit `ProptestConfig { cases: N, .. }` rather than relying on the default.
## Phase D — Re-benchmark + comparison
- [ ] Re-run `cargo bench -p mini-static` post-Range/hardening, compare against Phase A
baseline.
- [ ] Re-run `benches/load/run.sh` and `comparison/run_comparison.sh`, produce updated
markdown comparison table.
- [ ] Confirm `cargo ci` and `cargo lint` still pass.
---
## Verification checklist (recurring, check at every phase boundary)
- [ ] `cargo bench -p mini-static` completes cleanly, stable (non-bimodal) timings.
- [ ] `benches/load/run.sh` output: nonzero req/s, `p99 > p50 > 0`, consistent across two
consecutive runs.
- [ ] Comparison table generated with environment fingerprint (OS/CPU/tool versions).
- [ ] `curl -r 0-99 http://127.0.0.1:<port>/big.bin -o partial.bin` byte-exact against
`dd`-extracted expected bytes.
- [ ] `cargo test -p mini-static` — new proptest cases + existing traversal/header-
injection adversarial tests all pass.
- [ ] `cargo ci` and `cargo lint` pass at every commit.
## Open tradeoffs
1. nginx/static-web-server comparison: manual-only, never in `cargo ci` (recommended).
2. Fixture sidecar generation: shell out to system `gzip`/`brotli` vs. new dev-deps
(recommended: shell out).
3. Phase B minor-bump commit boundary: first-206-reachable vs. fully-documented commit
(recommended: first-reachable, per existing `DEV_PLAN.md` precedent).