From dfe642f68d5088b4ea388d05e3ff7cbc245ee97a Mon Sep 17 00:00:00 2001 From: Taemin Shin Date: Wed, 9 Sep 2026 11:54:40 +0900 Subject: [PATCH] Avoid replaying unread body data Retrying a body that was dropped before its first poll leaves an empty replay buffer. The retry reads and records the first DATA frame, then replays that same frame again on its next poll. Consume the empty replay phase before reading the original body, while preserving the existing capped-buffer error path. The new regression test failed with duplicated bytes before the fix. All 24 retry tests, all-target clippy, and formatting checks pass. Refs https://github.com/linkerd/linkerd2/issues/15649 Signed-off-by: Taemin Shin --- linkerd/http/retry/src/replay.rs | 1 + linkerd/http/retry/src/replay/tests.rs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/linkerd/http/retry/src/replay.rs b/linkerd/http/retry/src/replay.rs index 66ee9625e2..4a28806fa7 100644 --- a/linkerd/http/retry/src/replay.rs +++ b/linkerd/http/retry/src/replay.rs @@ -189,6 +189,7 @@ where tracing::trace!("Cannot replay buffered body, maximum buffer length reached"); return Poll::Ready(Some(Err(Capped.into()))); } + this.replay_body = false; } if this.replay_trailers { this.replay_trailers = false; diff --git a/linkerd/http/retry/src/replay/tests.rs b/linkerd/http/retry/src/replay/tests.rs index 3094b4b70f..e103ea5424 100644 --- a/linkerd/http/retry/src/replay/tests.rs +++ b/linkerd/http/retry/src/replay/tests.rs @@ -46,6 +46,23 @@ async fn replays_one_chunk() { } } +#[tokio::test] +async fn replays_unpolled_body() { + let Test { + mut tx, + initial, + replay, + _trace, + } = Test::new(); + tx.send_data("hello world").await; + drop(tx); + drop(initial); + + let (data, trailers) = body_to_string(replay).await; + assert_eq!(data, "hello world"); + assert_eq!(trailers, None); +} + #[tokio::test] async fn replays_several_chunks() { let Test {