From fc8b6e3a05a90e205aad2119ff9e220f3bc4b3fc Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:03:54 -0700 Subject: [PATCH] bump ureq 3.4.2 + preserve rolling-idle body timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ureq 3.4.1 (#1194, "Fix timeout budgets restarting and applying to later phases") made timeout_recv_response bound only the response HEADERS and turned timeout_recv_body into an ABSOLUTE total-body deadline. The online key-service POST set recv_response only, so under 3.4.2 the reply body read had NO deadline at all — a server that sent headers then stalled mid-reply would hang the key request forever. The /decode reply is a small JSON key answer that arrives in one read (a few hundred bytes; the 1 MiB cap is a defensive anti-OOM ceiling, not an expected size), so this is a fine total-cap case, not a large-body slow-link download: no rolling-idle connector is warranted. Set timeout_recv_body to the same TIMEOUT_SECS total ceiling so the POST can never hang forever. Regression guard the_reply_body_read_is_bounded_not_only_the_headers asserts recv_body is now set. Full suite + precommit (fmt/clippy/deny/tests) green. --- Cargo.lock | 4 ++-- src/online.rs | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19c4895..496334c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -946,9 +946,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.4.0" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" dependencies = [ "base64", "cookie_store", diff --git a/src/online.rs b/src/online.rs index 64dc9b2..c3e5ea5 100644 --- a/src/online.rs +++ b/src/online.rs @@ -287,6 +287,10 @@ fn hardened_agent(pinned: Vec) -> ureq::Agent { .max_redirects(0) .timeout_connect(Some(Duration::from_secs(10))) .timeout_recv_response(Some(Duration::from_secs(TIMEOUT_SECS))) + // Since ureq 3.4.1 (#1194) recv_response bounds only the HEADERS; without + // a body deadline a stalled reply hangs the POST forever. The JSON key + // answer arrives in one read, so a TOTAL cap is the right shape here. + .timeout_recv_body(Some(Duration::from_secs(TIMEOUT_SECS))) .build(); // `with_parts`, never `new_with_config` — see [`PinnedResolver`]. ureq::Agent::with_parts(config, DefaultConnector::new(), PinnedResolver(pinned)) @@ -917,6 +921,22 @@ mod tests { ); } + // The POST reply body has its own deadline, not just the headers: ureq + // 3.4.1+ recv_response covers headers only, so without recv_body a stalled + // reply would hang the key request forever. + #[test] + fn the_reply_body_read_is_bounded_not_only_the_headers() { + let agent = hardened_agent(Vec::new()); + let t = agent.config().timeouts(); + assert_eq!( + t.recv_body, + Some(Duration::from_secs(TIMEOUT_SECS)), + "ureq 3.4.1+ recv_response covers headers only; without recv_body the \ + reply read has no deadline at all" + ); + assert_eq!(t.recv_response, Some(Duration::from_secs(TIMEOUT_SECS))); + } + #[test] fn resolve_and_guard_accepts_public_literal() { // Public numeric hosts resolve without DNS — must be accepted.