From 89c55d11b6451da91ed7f216c54038cc57ecc36a Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Thu, 3 Sep 2026 14:28:51 -0700 Subject: [PATCH] windows: the request read deadline was not enforced on Windows Windows does not unblock a `recv` already parked in the kernel when another thread calls `shutdown` on the same socket, so the watchdog could not end a silent connection's read and it held its turnstile place for the whole 60s header timeout instead of the 10s deadline. Bound the read at the socket too, which enforces the same deadline everywhere; the watchdog stays as the backstop for a connection that trickles bytes without ever completing a request. POSIX behavior is unchanged: the watchdog already closed the socket at the deadline. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --- crates/live/src/live_http.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/live/src/live_http.rs b/crates/live/src/live_http.rs index 8fd48d5d7..b06f67305 100644 --- a/crates/live/src/live_http.rs +++ b/crates/live/src/live_http.rs @@ -459,6 +459,16 @@ pub fn read_request_deadline( max_body: usize, deadline: Duration, ) -> Option { + // Bound the read at the socket as well as at the watchdog. Windows does + // not unblock a `recv` already parked in the kernel when another thread + // calls `shutdown` on the same socket, so there the watchdog alone cannot + // enforce the deadline and a silent connection held its ticket for the + // whole 60s header timeout. A read that makes no progress for the deadline + // now returns on every platform and the handler drops its ticket; the + // watchdog stays as the backstop for a connection that keeps trickling + // bytes without ever completing a request. + let prev_timeout = stream.read_timeout().ok().flatten(); + let _ = stream.set_read_timeout(Some(deadline)); let done = Arc::new(AtomicBool::new(false)); let watch_stream = stream.try_clone().ok()?; let watch_done = done.clone(); @@ -482,6 +492,7 @@ pub fn read_request_deadline( }); let req = read_request(stream, max_body); done.store(true, Ordering::SeqCst); + let _ = stream.set_read_timeout(prev_timeout); req } @@ -968,12 +979,12 @@ mod tests { // Must run within a small multiple of the deadline. Before the fix the // silent ticket was held for the whole read window and this never // arrived; generous slack absorbs CI scheduling. The Windows runner - // schedules the watchdog's 50ms polling loop against a ~15.6ms system - // timer while the whole crate's tests run in parallel, so it needs a - // wider margin; the bound stays far under the 60s read timeout a + // schedules the watchdog's polling loop against a ~15.6ms system timer + // while the whole crate's tests run in parallel, so it gets a wider + // margin; the bound stays far under the 60s read timeout a // deadline-less read would hold the ticket for, so the test still // distinguishes the fix from the regression. - let slack = if cfg!(windows) { deadline * 40 } else { deadline * 6 }; + let slack = if cfg!(windows) { deadline * 12 } else { deadline * 6 }; ran_rx.recv_timeout(slack).expect( "later /events was wedged behind a silent connection past the read deadline", );