Skip to content

fix(http): add connection-level timeout protection against non-reading clients - #524

Merged
bug-ops merged 1 commit into
mainfrom
fix/523-conn-timeout-protection
Aug 19, 2026
Merged

fix(http): add connection-level timeout protection against non-reading clients#524
bug-ops merged 1 commit into
mainfrom
fix/523-conn-timeout-protection

Conversation

@bug-ops

@bug-ops bug-ops commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • apply_common_layers' TimeoutLayer/ResponseBodyTimeoutLayer (fix(http): remove dead frame streams, add timeout/concurrency limits #521) bound handler execution and the pre-response phase, but a client that stops reading the response socket is never caught: ResponseBodyTimeoutLayer's deadline only resets on body poll, and hyper stops polling once its write buffer fills waiting on the client to read. Closing that gap requires accounting at the TcpListener accept loop, which pjs-core previously had no API for (it only builds Routers; axum::serve is called by the embedding process).
  • Adds infrastructure::http::serve::{ConnectionLimits, serve_with_limits}, an http-server-gated replacement for axum::serve built on hyper_util::server::conn::auto::Builder. It bounds per-connection lifetime with a deadline around the connection future (default 300s, independent of body-poll activity — this is what closes the HTTP server has no connection-level protection against a non-reading client (residual #515 gap) #523 threat scenario), bounds header read time via header_read_timeout (default 10s, ~6x tighter than hyper's own 30s default, fixing a separate slowloris gap discovered during this work since axum::serve never configures a hyper timer), and caps concurrent connections via a semaphore acquired before accept() (default 1024, a distinct axis from fix(http): remove dead frame streams, add timeout/concurrency limits #521's handler-concurrency cap).
  • ConnectInfo<SocketAddr> is preserved per connection so the crate's own WebSocket upgrade handler and per-IP rate limiter keep working with the new serve function.
  • Accept-loop errors now retry with backoff (mirroring axum::serve's own behavior) instead of killing the whole server on a single transient error.
  • Wires all three pjs-demo server binaries to serve_with_limits in place of axum::serve; websocket_streaming.rs sets max_connection_duration: None to protect its long-lived /ws/{id} route.

This went through a full security-audit → implement → adversarial critique → fix → re-verify → code-review cycle. The critique caught two real correctness gaps in the first pass (dropped ConnectInfo, accept-error propagation killing the server) — both fixed and independently re-verified before this PR.

Test plan

  • cargo +nightly fmt --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo nextest run --workspace --all-features --lib --bins
  • cargo test --workspace --doc --all-features
  • RUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --no-deps --workspace --all-features
  • 6 new integration tests in crates/pjs-core/tests/http_serve_connection_limits.rs, including a regression test for the literal HTTP server has no connection-level protection against a non-reading client (residual #515 gap) #523 threat scenario (client completes a request against a large-body route, then stops reading — connection is force-closed within the configured deadline) and one for ConnectInfo availability to handlers
  • All three pjs-demo binaries build and run under the CI feature set

Closes #523
Closes #515

…g clients

apply_common_layers' TimeoutLayer/ResponseBodyTimeoutLayer bound handler
execution and the pre-response phase, but a client that stops reading
the response socket is never caught: ResponseBodyTimeoutLayer's deadline
only resets on body poll, and hyper stops polling once its write buffer
fills waiting on the client. Closing that gap requires accounting at the
TcpListener accept loop, which pjs-core previously had no API for.

Add infrastructure::http::serve::{ConnectionLimits, serve_with_limits},
an http-server-gated replacement for axum::serve built on
hyper_util::server::conn::auto::Builder. It bounds per-connection
lifetime with a deadline around the connection future (default 300s,
independent of body-poll activity), bounds header read time via
header_read_timeout (default 10s, fixing a separate slowloris gap since
axum::serve never configures a hyper timer), and caps concurrent
connections via a semaphore acquired before accept() (default 1024).
ConnectInfo<SocketAddr> is preserved per connection so the crate's own
WebSocket upgrade handler and per-IP rate limiter keep working.

Wire all three pjs-demo server binaries to serve_with_limits in place
of axum::serve; websocket_streaming.rs sets max_connection_duration to
None to protect its long-lived /ws/{id} route.

Closes #523
Closes #515
@bug-ops
bug-ops enabled auto-merge (squash) August 19, 2026 00:13
@github-actions github-actions Bot added documentation Documentation updates, README, guides dependencies Dependency updates (Cargo.toml, Cargo.lock) core Changes to pjs-core crate infrastructure Infrastructure layer changes (Clean Architecture) security Security issues or improvements testing Test coverage improvements, new test cases rust Rust language specific changes pjs-demo labels Aug 19, 2026
@github-actions

Copy link
Copy Markdown

WASM Bundle Size Report

Target Raw (KB) Gzipped (KB) Status
web 154 69 PASS
nodejs 154 69 PASS
bundler 154 69 PASS

@bug-ops
bug-ops merged commit 8a85ca1 into main Aug 19, 2026
54 checks passed
@bug-ops
bug-ops deleted the fix/523-conn-timeout-protection branch August 19, 2026 00:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Changes to pjs-core crate dependencies Dependency updates (Cargo.toml, Cargo.lock) documentation Documentation updates, README, guides infrastructure Infrastructure layer changes (Clean Architecture) pjs-demo rust Rust language specific changes security Security issues or improvements testing Test coverage improvements, new test cases

Projects

None yet

1 participant