From e52024be4b82500343e9987f6724aa928c027107 Mon Sep 17 00:00:00 2001 From: Swanand Mulay <73115739+swanandx@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:04:47 +0530 Subject: [PATCH] fix(layers/retry): do not reopen a read once the range is exhausted When a stream breaks after its last byte, the layer advances the range to size zero and reopens with it. Nothing is left to fetch, and an empty range has no valid Range header. Return an empty buffer instead, which callers treat as end of stream. An unbounded read has no size to compare against, so it still reopens past the end. Catching that needs the object size. --- core/layers/retry/src/lib.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/core/layers/retry/src/lib.rs b/core/layers/retry/src/lib.rs index 13a433c4c327..5a1e6b5535cb 100644 --- a/core/layers/retry/src/lib.rs +++ b/core/layers/retry/src/lib.rs @@ -657,6 +657,12 @@ impl oio::ReadStream for RetryReadStream stream, Err(err) => return ((None, range, read), Err(err)), @@ -1450,6 +1456,32 @@ mod tests { Ok(()) } + /// A stream that breaks after its last byte is at EOF, so the layer must end + /// the read instead of reopening the empty range left over. `test_retry_read` + /// covers the unbounded case, which has no size to stop on and still reopens. + #[tokio::test] + async fn test_retry_read_bounded_range_does_not_reopen_at_eof() -> Result<()> { + setup(); + + let builder = MockBuilder::default(); + let op = Operator::new(builder.clone())? + .layer(LoggingLayer::default()) + .layer(RetryLayer::default()); + + let r = op.reader("retryable_error").await?; + let mut content = Vec::new(); + let size = r + .read_into(&mut content, 0..13) + .await + .expect("read must succeed"); + assert_eq!(size, 13); + assert_eq!(content, "Hello, World!".as_bytes()); + // Two retries, one full read, then the error at EOF. No reopen, so no + // fifth attempt. + assert_eq!(*builder.attempt.lock().unwrap(), 4); + Ok(()) + } + /// This test is used to reproduce the panic issue while composing retry layer with timeout layer. #[tokio::test] async fn test_retry_write_fail_on_close() -> Result<()> {