Files
alkhttp/tasks/server/review-001-hyper-server-knobs.md
T
glm-5.3-flash 314472012d fix(server): hyper knobs, decoy fidelity, cache + builder fixes (SRV-04..SRV-10)
- SRV-04: TokioTimer on h1+h2 builder, header_read_timeout 10s,
  h1 keep-alive on, h2 keep-alive 30s/10s; concurrency boundary documented
- SRV-05: with_decoy rebuild keeps extra routes (clone, not take)
- SRV-07: method_not_allowed_fallback serves the nginx-shaped 405
- SRV-08: UTF-8 percent-decoding, literal '+', tokio::fs syscalls
- SRV-09: /openapi.json cached at construction, generic 500 body,
  to_openapi returns Result (expect removed)
- SRV-10: ChannelsPolicy extension injection point on the WS upgrade;
  single token resolution via route ordering (WS layer before the
  router-wide auth route_layer)

Verification: cargo test 265 passed; --all-features server::/to_openapi::
green; clippy + fmt clean on touched files (remaining tree noise is a
parallel agent's in-flight from_mcp/from_wss/forward work)
2026-08-29 10:47:44 +00:00

6.1 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
review-001-hyper-server-knobs Configure hyper timeouts + decoy/405/proxy-path fixes (SRV-04, SRV-07, SRV-08, SRV-09, SRV-10) completed
moderate low component implementation
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

  • Hyper configured with timer + header read timeout; knob values documented
  • 405 responses carry the decoy Server header (test with OPTIONS /search)
  • %C3%A9 and a+b.html resolve correctly in the decoy static server (tests); async-path syscalls gone
  • /openapi.json 500 body is generic; doc cached; expect removed
  • Second .with_decoy no longer silently drops extra routes (test)
  • SRV-10: policy injection point exists or doc corrected; single token resolution
  • 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<OpenAPISpec, AdapterError> — 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<dyn ChannelLifecyclePolicy>) 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.