Fix streaming-body watchdog bug (review #009 C2) + dead-upstream test wiring (C3)

C2: IdleTrackingService decremented in_flight when the handler returned
the Response, but for streaming responses (e.g. git clone) the body is
still in flight, so the watchdog could kill the connection mid-stream.
Fix: wrap the response body in IdleTrackingBody which owns the decrement,
releasing it on body EOF/error/drop (guarded against double-decrement by
an AtomicBool) and calling touch() on each data frame. The handler-level
decrement now only happens on the Err branch. Verified by a reproducer
test (streaming_body_not_killed_by_idle_watchdog) that streams 15 chunks
over 3s with a 500ms idle timeout — red on 4ab8c51, green with this fix.

C3: the two pre-existing idle-timeout tests pointed at 127.0.0.1:18080
where nothing listens, so they validated watchdog behaviour against an
immediate 504 error response (HTTP/1.1 prefix + in_flight==1 hold for
a 504 just as for a 200) and never exercised a real upstream round-trip.
Rewired them to spawn a real TestUpstream and assert HTTP/1.1 200 OK.
Removed the dead no-arg make_proxy_router/start_test_https_server wrappers.

Adds http-body as a direct dep (for the Frame/Body trait) and
tokio-stream as a dev-dep (for the slow-stream test helper).

cargo test: 226 unit + 40 integration green; cargo clippy --all-targets clean.

C1 (deploy to dev1) remains open and out of scope for this repo session.
This commit is contained in:
glm-5.2 committed 2026-08-19 09:48:13 +00:00
1 parent ff8819e950
commit 71ad3c2905
6 files changed
+366 -23

No files matched your search

+32
View File
@@ -1,4 +1,5 @@
use std::net::SocketAddr;
use std::time::Duration;
use axum::routing::get;
use axum::Router;
@@ -34,4 +35,35 @@ impl TestUpstream {
pub async fn spawn_ok() -> Self {
Self::spawn(|| Router::new().route("/", get(|| async { "ok" }))).await
}
/// Upstream that emits one chunk every `chunk_interval` for `num_chunks`
/// chunks, then ends. The body stream outlasts the idle timeout so the
/// watchdog's behaviour for in-progress streaming responses is observable.
/// Emits `b"<n>"` per chunk so the client can verify it received the whole
/// stream.
pub async fn spawn_slow_stream(chunk_interval: Duration, num_chunks: usize) -> Self {
Self::spawn(move || {
let interval = chunk_interval;
let n = num_chunks;
Router::new().route(
"/",
get(move || async move {
let (tx, rx) = tokio::sync::mpsc::channel::<
Result<axum::body::Bytes, std::convert::Infallible>,
>(16);
tokio::spawn(async move {
for i in 0..n {
tokio::time::sleep(interval).await;
let _ = tx
.send(Ok(format!("<{i}>").into_bytes().into()))
.await;
}
});
let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
axum::body::Body::from_stream(stream)
}),
)
})
.await
}
}