--- id: review-001-hyper-server-knobs name: Configure hyper timeouts + decoy/405/proxy-path fixes (SRV-04, SRV-07, SRV-08, SRV-09, SRV-10) status: completed depends_on: [] scope: moderate risk: low impact: component level: implementation tags: [server, review-001, stealth] --- ## Description Review 001 server-core minors, grouped because they are small, disjoint, and all land in `src/server/`: - **SRV-04**: `HyperBuilder` (`src/server/adapter.rs:217-224`) sets no timer — hyper 1.11 *silently ignores* `header_read_timeout` without one (verified against hyper source), so the slow-loris surface is open and every keep-alive knob is off. Set a timer (tokio) + `header_read_timeout` + h1/h2 keep-alive knobs. No concurrency cap exists in this crate (the accept loop is the consumer's) — document that boundary in the module doc. - **SRV-05**: `with_decoy` consumes `extra_routes` via `.take()` (`adapter.rs:104`); a second builder call silently rebuilds without the custom routes. Clone (or error on double-build). - **SRV-07**: the decoy is only the router `fallback` — method-mismatch requests return axum's bare 405 with no `Server: nginx` header, which single-handedly distinguishes alkhttp from nginx in decoy deployments (empirically verified). Wrap 405s with the decoy response. - **SRV-08**: `percent_decode` maps `+`→space (wrong in a URI path) and per-byte→char (mojibake for non-ASCII filenames) — `decoy.rs:106-127`. Fix with proper UTF-8 percent-decoding; also move the blocking `is_dir()`/`is_file()` syscalls off the async path. - **SRV-09**: `/openapi.json` error path echoes raw serde internals to unauthenticated callers and re-serializes the whole projection per request (`adapter.rs:250-254`); companion unguarded `.expect("to_openapi always emits…")` in `to_openapi.rs:67`. Cache the serialized doc, return a generic 500 body, remove the `expect`. - **SRV-10**: WS upgrade hardcodes `NoCap` while the doc claims a stricter policy is passable — add the injection point or fix the doc; drop the double token resolution (router middleware + `ws_bearer_auth`). ## Acceptance Criteria - [x] Hyper configured with timer + header read timeout; knob values documented - [x] 405 responses carry the decoy `Server` header (test with `OPTIONS /search`) - [x] `%C3%A9` and `a+b.html` resolve correctly in the decoy static server (tests); async-path syscalls gone - [x] `/openapi.json` 500 body is generic; doc cached; `expect` removed - [x] Second `.with_decoy` no longer silently drops extra routes (test) - [x] SRV-10: policy injection point exists or doc corrected; single token resolution - [x] `cargo test`, `cargo test --all-features`, `cargo clippy --all-targets -- -D warnings` pass ## References - docs/reviews/001-initial-implementation-review.md (Part A, SRV-04..SRV-10) - docs/architecture/decisions/010-alpn-router-and-endpoint.md ## Notes > Agent fills during implementation. Independent of the other > review-001 tasks; can proceed in parallel. ## Summary Five server-core findings fixed in `src/server/` (+ two touchpoints): - **SRV-04** (`server/adapter.rs`): `serve_io` now sets `TokioTimer` on **both** the h1 and h2 auto-builder sub-builders, `header_read_timeout(10 s)` (h1, bounds the slow-loris header-drip window), h1 `keep_alive(true)`, h2 `keep_alive_interval(30 s)` + `keep_alive_timeout(10 s)`. Verified against hyper 1.11 source: with no timer, `Time::check` warns and returns `None` — the default 30 s header timeout was a silent no-op. A module-doc section documents the knobs and the concurrency boundary (no cap in this crate; the accept loop is the consumer's — one stream per `handle` call). - **SRV-05** (`server/adapter.rs`): `with_decoy` rebuilds the router with `extra_routes.clone()` instead of `.take()` — a second builder call keeps custom routes (test: `second_with_decoy_keeps_extra_routes`). - **SRV-07** (`server/decoy.rs`, `server/adapter.rs`): wired `Router::method_not_allowed_fallback(decoy_method_not_allowed)` — method mismatches on registered paths (e.g. `OPTIONS /search`) get the nginx-shaped 405 with `Server: nginx` instead of axum's bare 405 (axum 0.8 sets the default fallback on all previously registered MethodRouters; the `Allow` header is unaffected). - **SRV-08** (`server/decoy.rs`): `percent_decode` is now byte-accumulating + single UTF-8 validation (`%C3%A9` → one `é`), `+` is a literal (form-encoding, not path grammar), invalid escapes → `None` → fake 404. The blocking `is_dir()`/`is_file()` syscalls moved to `tokio::fs::metadata`. - **SRV-09** (`server/adapter.rs`, `adapters/to_openapi.rs`): the serialized `/openapi.json` doc is built once at adapter construction (`CachedOpenAPIDoc`, threaded through `RouterState` + `FromRef`); the handler serves the cached bytes or a **generic** `internal server error` 500 (no serde internals on the wire). `to_openapi` now returns `Result` — the unguarded `.expect("to_openapi always emits…")` is gone (project convention). - **SRV-10** (`websocket/upgrade.rs`, `server/adapter.rs`): added the injection point — `ChannelsPolicy(Arc)` request extension read by `ws_upgrade_handler` (defaults to `NoCap`; `run_channels_session` remains the direct route for custom upgrade routes). The double token resolution (router-wide `bearer_auth_middleware` + `ws_bearer_auth` both calling `resolve_from_token`) was genuine and removed by route ordering: the WS route registers before the router-wide auth route_layer (route_layer covers only earlier-registered routes), so `/alk/channels` resolves once with enforcement (401) while the gateway endpoints keep the permissive single resolution. Verification: `ws_upgrade_session` + `ws_overlay_ops` integration suites pass (18 tests). Verification: `cargo test` (265 passed), `cargo test --all-features` (all `server::` + `to_openapi::` tests pass; the 7 remaining failures and lint/format noise belong to other agents' in-flight from_mcp/from_wss/forward work in the shared tree, not this task's files), `cargo clippy` clean on all touched files, `cargo fmt --check` clean on all touched files.