fix(http): add connection-level timeout protection against non-reading clients - #524
Merged
Conversation
…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
enabled auto-merge (squash)
August 19, 2026 00:13
This was referenced Aug 19, 2026
Closed
WASM Bundle Size Report
|
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 theTcpListeneraccept loop, whichpjs-corepreviously had no API for (it only buildsRouters;axum::serveis called by the embedding process).infrastructure::http::serve::{ConnectionLimits, serve_with_limits}, anhttp-server-gated replacement foraxum::servebuilt onhyper_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 viaheader_read_timeout(default 10s, ~6x tighter than hyper's own 30s default, fixing a separate slowloris gap discovered during this work sinceaxum::servenever configures a hyper timer), and caps concurrent connections via a semaphore acquired beforeaccept()(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.axum::serve's own behavior) instead of killing the whole server on a single transient error.pjs-demoserver binaries toserve_with_limitsin place ofaxum::serve;websocket_streaming.rssetsmax_connection_duration: Noneto 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 --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo nextest run --workspace --all-features --lib --binscargo test --workspace --doc --all-featuresRUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --no-deps --workspace --all-featurescrates/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 forConnectInfoavailability to handlerspjs-demobinaries build and run under the CI feature setCloses #523
Closes #515