Skip to content

Avoid retrying object_exists 404 responses#458

Merged
durch merged 1 commit into
masterfrom
fix/issue-444-object-exists-404
May 4, 2026
Merged

Avoid retrying object_exists 404 responses#458
durch merged 1 commit into
masterfrom
fix/issue-444-object-exists-404

Conversation

@durch
Copy link
Copy Markdown
Owner

@durch durch commented May 4, 2026

Linked issue: Fixes #444

Problem

Bucket::object_exists used response_data(false), whose backend path wraps response() in retry!. With fail-on-err, an expected HEAD 404 is converted to an error before object_exists can classify it, so the retry macro delays and logs warnings for a terminal “object missing” result.

Solution

Add an internal Request::response_status hook for status-only calls and have object_exists use it. Built-in tokio, async-std, and sync backends classify status 404 as Ok(404) before retry! sees an error, while preserving existing fail-on-err behavior for non-404 failures. object_exists now returns Ok(false) for 404 and Ok(true) for any other successful status.

Focused verification

  • cargo test -p rust-s3 test_object_exists_404_does_not_retry --features tokio-native-tls -- --nocapture — passed; regression test observed exactly one local HEAD request for a 404.
  • cargo check -p rust-s3 --features tokio-native-tls,fail-on-err — passed.
  • cargo check -p rust-s3 --no-default-features --features sync-native-tls,fail-on-err — passed.
  • cargo check -p rust-s3 --no-default-features --features async-std-native-tls,fail-on-err — passed.

Risk notes

  • Retry behavior is preserved for transient request errors because response_status remains wrapped in retry!.
  • fail-on-err semantics for ordinary response/body methods are unchanged; only object_exists moves to the status-only path.
  • Provider/runtime cordons: no signing, credential resolution, URL style, or public Bucket API changes. The new trait method has a default implementation to avoid breaking external implementors, with built-in backend overrides for the no-retry 404 behavior.

Self-review

  • Correctness: expected HEAD 404 is terminal Ok(false) before retry; non-404 failures still return errors through backend fail-on-err logic.
  • Compatibility: public object_exists signature and ordinary method behavior unchanged.
  • Risk area: request backend code duplicated status-only request construction; focused checks covered tokio, async-std, and sync feature splits.
  • Missing tests: focused regression is tokio-backed only; async-std/sync are compile-checked but not behavior-tested with local servers.
  • Merge readiness: ready for maintainer review; I did not approve or merge this PR.

This change is Reviewable

Summary by CodeRabbit

  • Bug Fixes

    • Object-existence checks now treat 404 responses as "not found" immediately, avoiding extra retries and reducing false-positive results.
    • Status-only request path added to streamline status handling and error conversion.
  • Tests

    • New test ensures a single request is made and that a 404 response yields a false "exists" result.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

Bucket::object_exists now calls Request::response_status() and returns false only for HTTP 404; Request gained a response_status implementation in tokio, async-std, and blocking backends that executes the request under existing retry logic but treats 404 as a non-retryable success path. A Tokio test verifies no retry on 404.

Changes

Status Extraction Optimization

Layer / File(s) Summary
Trait Definition
s3/src/request/request_trait.rs
Added async fn response_status(&self) -> Result<u16, S3Error> default method that returns the numeric status code via response_header(); establishes API used by callers.
Backend Implementations
s3/src/request/tokio_backend.rs, s3/src/request/async_std_backend.rs, s3/src/request/blocking.rs
Each backend implements response_status to build and send the HTTP request inside crate::retry!, return the numeric status, immediately return Ok(404) for 404 responses, and (when fail-on-err is enabled) convert other non-success statuses into S3Error::HttpFailWithBody/HttpFail.
Core Usage
s3/src/bucket.rs
Bucket::object_exists updated to call request.response_status().await? and return Ok(status != 404), replacing the previous response_data() + special-case error handling that caused retries on 404.
Tests
s3/src/bucket.rs (test section)
Added a Tokio-only test test_object_exists_404_does_not_retry that runs a local TCP server returning fixed 404 responses, asserts object_exists returns false, and verifies exactly one request was made (no retry).

Sequence Diagram(s)

sequenceDiagram
  participant Bucket as Bucket::object_exists
  participant Req as Request::response_status
  participant Retry as crate::retry!
  participant Server as Remote S3 / TCP server

  Bucket->>Req: call response_status(path)
  Req->>Retry: execute HTTP request
  Retry->>Server: TCP/HTTP request
  Server-->>Retry: 404 Not Found
  Retry-->>Req: returns 404 status
  Req-->>Bucket: Ok(404)
  Bucket-->>Bucket: returns false (status == 404)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

I hopped to check a missing file today,
One 404 and I hopped away,
No needless retry, no circular fuss,
A single knock, then peace for us. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: avoiding unnecessary retries of 404 responses when checking object existence.
Linked Issues check ✅ Passed The changes implement the core requirement from issue #444: preventing unnecessary retries of expected 404 responses in object_exists by introducing response_status method that treats 404 as Ok(404) before retry! sees an error.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing issue #444: adding response_status trait method and implementations across backends, updating object_exists to use it, and adding regression test.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-444-object-exists-404

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 3/8 reviews remaining, refill in 36 minutes and 25 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@brownian-motion-v0
Copy link
Copy Markdown

Brownian Motion (Brass)

Recommendation: Proceed

Summary: PR effectively addresses unnecessary retries for 404 in object_exists.
Risk: Medium · Confidence: 80%

Highlights

  • Good test coverage
  • Clear commit history

Unknowns

  • Missing tests for async-std/sync behavior

Next actions

  • Keep: the new response_status method for clarity
  • Drop: any redundant error handling in object_exists
  • Add: tests for async-std and sync backends

Reflection questions

  • What core assumption underpins this PR's approach?
  • How does this change align with the project's longer-term goals?
  • Could there be a simpler way to achieve the primary objective here?

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
s3/src/request/request_trait.rs (1)

213-216: ⚡ Quick win

Document the override contract for response_status().

This new public trait method currently relies on callers discovering from the backend impls that the default path just delegates to response_header(). Please add rustdoc with an example and explicitly note that backends should override it when they need status-only classification before error/retry handling kicks in.

As per coding guidelines, "All public APIs should have documentation examples".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@s3/src/request/request_trait.rs` around lines 213 - 216, Add rustdoc for the
public trait method response_status() in request_trait.rs that documents its
override contract: explain that the default implementation simply delegates to
response_header(), show a short example snippet (illustrating overriding
response_status() to return just the status code without calling
response_header()), and explicitly state that backend implementations should
override response_status() when they need status-only classification prior to
error/retry handling; reference the related method response_header() in the doc
and include a one-line code example per the public API docs guideline.
s3/src/bucket.rs (1)

3123-3174: ⚡ Quick win

Add the same no-retry regression test for async-std and blocking.

The behavior change lives in three backend-specific response_status() overrides, but only the Tokio path is runtime-tested here. Compile checks won't catch a backend-specific regression in the 404 classification/retry behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@s3/src/bucket.rs` around lines 3123 - 3174, The new Tokio-only test
test_object_exists_404_does_not_retry only verifies the no-retry 404 behavior
for the Tokio backend, so add equivalent tests for the async-std and blocking
runtimes to prevent backend-specific regressions: create duplicate async tests
decorated with #[cfg(all(not(feature = "sync"), feature = "with-async-std"))]
and #[async_std::test] using the same logic as
test_object_exists_404_does_not_retry, and a blocking test under #[cfg(feature =
"sync")] (e.g., using std::thread::spawn and blocking Bucket::object_exists
calls) so the three backend-specific response_status() overrides are covered;
ensure each test uses the same request counter, TcpListener endpoint,
crate::set_retries calls, and assertions (exists is false and requests == 1).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@s3/src/bucket.rs`:
- Around line 1028-1029: The current check treats any non-404 status as
existence because it returns Ok(status_code != 404); change this to return
Ok(true) only when the response status is a success (2xx) and return Ok(false)
for 404, but surface any other status as an error. Locate the call to
request.response_status().await? and replace the boolean expression with a
branch that uses status.is_success() (or checks 200..=299) to return Ok(true),
explicitly handle 404 to return Ok(false), and convert all other status codes
into an Err (propagating or mapping into the function’s error type) so
auth/server errors aren’t masked.

---

Nitpick comments:
In `@s3/src/bucket.rs`:
- Around line 3123-3174: The new Tokio-only test
test_object_exists_404_does_not_retry only verifies the no-retry 404 behavior
for the Tokio backend, so add equivalent tests for the async-std and blocking
runtimes to prevent backend-specific regressions: create duplicate async tests
decorated with #[cfg(all(not(feature = "sync"), feature = "with-async-std"))]
and #[async_std::test] using the same logic as
test_object_exists_404_does_not_retry, and a blocking test under #[cfg(feature =
"sync")] (e.g., using std::thread::spawn and blocking Bucket::object_exists
calls) so the three backend-specific response_status() overrides are covered;
ensure each test uses the same request counter, TcpListener endpoint,
crate::set_retries calls, and assertions (exists is false and requests == 1).

In `@s3/src/request/request_trait.rs`:
- Around line 213-216: Add rustdoc for the public trait method response_status()
in request_trait.rs that documents its override contract: explain that the
default implementation simply delegates to response_header(), show a short
example snippet (illustrating overriding response_status() to return just the
status code without calling response_header()), and explicitly state that
backend implementations should override response_status() when they need
status-only classification prior to error/retry handling; reference the related
method response_header() in the doc and include a one-line code example per the
public API docs guideline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7349aff-d15a-4e27-9643-ab32617a8679

📥 Commits

Reviewing files that changed from the base of the PR and between 57525a4 and 8713dcb.

📒 Files selected for processing (5)
  • s3/src/bucket.rs
  • s3/src/request/async_std_backend.rs
  • s3/src/request/blocking.rs
  • s3/src/request/request_trait.rs
  • s3/src/request/tokio_backend.rs

Comment thread s3/src/bucket.rs
Comment on lines +1028 to +1029
let status_code = request.response_status().await?;
Ok(status_code != 404)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only treat successful HEAD responses as existence.

response_status() returns raw non-2xx statuses when fail-on-err is disabled, so this now reports Ok(true) for cases like 403, 500, or a redirect. That hides auth/server problems as “object exists”. Please keep 404 => false, return true only for success statuses, and surface every other status as an error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@s3/src/bucket.rs` around lines 1028 - 1029, The current check treats any
non-404 status as existence because it returns Ok(status_code != 404); change
this to return Ok(true) only when the response status is a success (2xx) and
return Ok(false) for 404, but surface any other status as an error. Locate the
call to request.response_status().await? and replace the boolean expression with
a branch that uses status.is_success() (or checks 200..=299) to return Ok(true),
explicitly handle 404 to return Ok(false), and convert all other status codes
into an Err (propagating or mapping into the function’s error type) so
auth/server errors aren’t masked.

@durch durch force-pushed the fix/issue-444-object-exists-404 branch from 8713dcb to 4b6cc05 Compare May 4, 2026 20:47
@brownian-motion-v0
Copy link
Copy Markdown

Brownian Motion (Brass)

Recommendation: Proceed

Summary: PR effectively addresses unnecessary retries for 404 responses in object_exists.
Risk: Medium · Confidence: 80%

Highlights

  • Good test coverage
  • Clear commit history

Unknowns

  • No performance impact assessment provided

Next actions

  • Keep: the new response_status method for clarity.
  • Drop: any redundant logging related to retries.
  • Add: tests for async-std and sync backends to ensure full coverage.

Reflection questions

  • What core assumption underpins this PR's approach?
  • How does this change align with the project's longer-term goals?
  • Could there be a simpler way to achieve the primary objective here?

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
s3/src/bucket.rs (1)

1028-1029: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Non-404, non-2xx statuses are silently reported as "object exists".

When fail-on-err is disabled (the default), response_status() returns Ok(status) for statuses like 403, 500, or 301. The expression status_code != 404 then evaluates to true, so object_exists returns Ok(true) for permission errors, server errors, and redirects — masking real problems as "object exists".

The fix belongs in object_exists: treat only 2xx as existence and surface everything else as an error:

🐛 Proposed fix
-        let status_code = request.response_status().await?;
-        Ok(status_code != 404)
+        match request.response_status().await? {
+            404 => Ok(false),
+            s if (200..300).contains(&(s as u32)) => Ok(true),
+            s => Err(S3Error::HttpFailWithBody(s, String::new())),
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@s3/src/bucket.rs` around lines 1028 - 1029, The current object_exists
implementation treats any non-404 response as existence because it returns
Ok(status_code != 404); change object_exists to consider only 2xx responses as
existence and treat all other statuses (including 3xx, 4xx != 404, 5xx) as
errors: call response_status(), check status.is_success() (or status.as_u16() in
range 200..=299) and return Ok(true) only for 2xx, return Ok(false) only for
404, and for any other status return Err(...) (propagate or map the non-2xx
status into a descriptive error). Update logic around response_status() and the
object_exists function to surface non-2xx statuses instead of reporting them as
existence.
🧹 Nitpick comments (1)
s3/src/request/tokio_backend.rs (1)

120-168: 🏗️ Heavy lift

response_status duplicates the entire request-building block from response().

The ~20-line sequence (header construction, client retrieval, method matching, URL/body assembly, client.execute) is copied verbatim from response() (lines 76–109). Any future change to request construction must be maintained in two places.

response_status() cannot simply delegate to self.response() because response() already applies the fail-on-err check and would convert a 404 into Err before response_status can intercept it. The clean fix is to extract a private execute_raw helper that builds and fires the request without the fail-on-err guard, then have both methods call it:

♻️ Suggested refactor
+    // Private: build and fire the request without any error-on-status logic.
+    async fn execute_raw(&self) -> Result<reqwest::Response, S3Error> {
+        let headers = self
+            .headers()
+            .await?
+            .iter()
+            .map(|(k, v)| {
+                (
+                    reqwest::header::HeaderName::from_str(k.as_str()),
+                    reqwest::header::HeaderValue::from_str(v.to_str().unwrap_or_default()),
+                )
+            })
+            .filter(|(k, v)| k.is_ok() && v.is_ok())
+            .map(|(k, v)| (k.unwrap(), v.unwrap()))
+            .collect();
+
+        let client = self.bucket.http_client();
+        let method = match self.command.http_verb() {
+            HttpMethod::Delete => reqwest::Method::DELETE,
+            HttpMethod::Get    => reqwest::Method::GET,
+            HttpMethod::Post   => reqwest::Method::POST,
+            HttpMethod::Put    => reqwest::Method::PUT,
+            HttpMethod::Head   => reqwest::Method::HEAD,
+        };
+        let request = client
+            .request(method, self.url()?.as_str())
+            .headers(headers)
+            .body(self.request_body()?)
+            .build()?;
+        Ok(client.execute(request).await?)
+    }

     async fn response(&self) -> Result<Self::Response, S3Error> {
-        let headers = self
-            .headers()
-            // ... (full duplicated block)
-        let response = client.execute(request).await?;
+        let response = self.execute_raw().await?;
         if cfg!(feature = "fail-on-err") && !response.status().is_success() {
             let status = response.status().as_u16();
             let text = response.text().await?;
             return Err(S3Error::HttpFailWithBody(status, text));
         }
         Ok(response)
     }

     async fn response_status(&self) -> Result<u16, S3Error> {
         retry! {
             async {
-                let headers = self
-                    .headers()
-                    // ... (full duplicated block)
-                let response = client.execute(request).await?;
-                let status = response.status().as_u16();
+                let response = self.execute_raw().await?;
+                let status = response.status().as_u16();
                 if status == 404 {
                     return Ok(status);
                 }
                 if cfg!(feature = "fail-on-err") && !response.status().is_success() {
                     let text = response.text().await?;
                     return Err(S3Error::HttpFailWithBody(status, text));
                 }
                 Ok(status)
             }.await
         }
     }

Note: execute_raw should live in a separate impl<'a> ReqwestRequest<'a> block (outside the #[maybe_async]-annotated trait impl) so it isn't subject to the trait's async/sync transformation. The same pattern should be applied to the analogous backends (async_std_backend.rs, blocking.rs).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@s3/src/request/tokio_backend.rs` around lines 120 - 168, response_status
currently duplicates request construction from response; extract a private
helper (e.g., execute_raw) on the ReqwestRequest type that builds and executes
the reqwest Request and returns the raw Response (without applying the
fail-on-err logic or converting 404s), place execute_raw in a separate impl<'a>
ReqwestRequest<'a> block (outside the #[maybe_async] trait impl) so it won't be
transformed, then refactor response() to call execute_raw and apply the existing
fail-on-err handling there, and refactor response_status() to call execute_raw
and inspect/return the status (handling 404 specially) — replicate this pattern
for async_std_backend.rs and blocking.rs as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@s3/src/bucket.rs`:
- Around line 3123-3174: The test test_object_exists_404_does_not_retry modifies
global retry state via crate::set_retries(1) without restoring it, which can
leak into other tests; change the test to capture the current retry count before
setting (e.g., let prev = crate::get_retries() or otherwise read the current
value), call crate::set_retries(1) for the duration of the test, and then
restore the original value at the end (crate::set_retries(prev)) — ensure
restoration runs even if the test panics by using a guard/RAII pattern or a
finally-style drop guard inside test_object_exists_404_does_not_retry so the
global retries are returned to their original value.

---

Duplicate comments:
In `@s3/src/bucket.rs`:
- Around line 1028-1029: The current object_exists implementation treats any
non-404 response as existence because it returns Ok(status_code != 404); change
object_exists to consider only 2xx responses as existence and treat all other
statuses (including 3xx, 4xx != 404, 5xx) as errors: call response_status(),
check status.is_success() (or status.as_u16() in range 200..=299) and return
Ok(true) only for 2xx, return Ok(false) only for 404, and for any other status
return Err(...) (propagate or map the non-2xx status into a descriptive error).
Update logic around response_status() and the object_exists function to surface
non-2xx statuses instead of reporting them as existence.

---

Nitpick comments:
In `@s3/src/request/tokio_backend.rs`:
- Around line 120-168: response_status currently duplicates request construction
from response; extract a private helper (e.g., execute_raw) on the
ReqwestRequest type that builds and executes the reqwest Request and returns the
raw Response (without applying the fail-on-err logic or converting 404s), place
execute_raw in a separate impl<'a> ReqwestRequest<'a> block (outside the
#[maybe_async] trait impl) so it won't be transformed, then refactor response()
to call execute_raw and apply the existing fail-on-err handling there, and
refactor response_status() to call execute_raw and inspect/return the status
(handling 404 specially) — replicate this pattern for async_std_backend.rs and
blocking.rs as well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6be0e67-4f0b-4434-9b93-2cc4561c3608

📥 Commits

Reviewing files that changed from the base of the PR and between 8713dcb and 4b6cc05.

📒 Files selected for processing (5)
  • s3/src/bucket.rs
  • s3/src/request/async_std_backend.rs
  • s3/src/request/blocking.rs
  • s3/src/request/request_trait.rs
  • s3/src/request/tokio_backend.rs
✅ Files skipped from review due to trivial changes (1)
  • s3/src/request/blocking.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • s3/src/request/async_std_backend.rs
  • s3/src/request/request_trait.rs

Comment thread s3/src/bucket.rs
Comment on lines +3123 to +3174
#[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
#[tokio::test]
async fn test_object_exists_404_does_not_retry() {
init();

let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let endpoint = format!("http://{}", listener.local_addr().unwrap());
let requests = Arc::new(AtomicUsize::new(0));
let request_count = Arc::clone(&requests);

let server = thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
request_count.fetch_add(1, Ordering::SeqCst);

let mut buffer = [0; 2048];
let _ = stream.read(&mut buffer).unwrap();
stream
.write_all(
b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
)
.unwrap();
});

crate::set_retries(1);

let credentials = Credentials::new(
Some("test_access_key"),
Some("test_secret_key"),
None,
None,
None,
)
.unwrap();
let bucket = Bucket::new(
"test-bucket",
Region::Custom {
region: "us-east-1".to_owned(),
endpoint,
},
credentials,
)
.unwrap()
.with_path_style();

let exists = bucket.object_exists("/missing.txt").await.unwrap();

crate::set_retries(1);
server.join().unwrap();

assert!(!exists);
assert_eq!(requests.load(Ordering::SeqCst), 1);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

LGTM — test correctly enforces the no-retry invariant.

The single-accept server design is intentional and effective: if object_exists retried, the second attempt would receive ECONNREFUSED, causing .unwrap() at line 3167 to panic and failing the test. The requests counter then provides a precise assertion that only one request was made.

One note: crate::set_retries(1) at line 3169 is global state and does not restore the previous default — it just sets the same value that was already set at line 3146. If the library's default retry count is greater than 1, tests running after this one in the same process will unexpectedly inherit retries = 1. Consider saving and restoring the original value, or using a test-local mechanism if the API supports it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@s3/src/bucket.rs` around lines 3123 - 3174, The test
test_object_exists_404_does_not_retry modifies global retry state via
crate::set_retries(1) without restoring it, which can leak into other tests;
change the test to capture the current retry count before setting (e.g., let
prev = crate::get_retries() or otherwise read the current value), call
crate::set_retries(1) for the duration of the test, and then restore the
original value at the end (crate::set_retries(prev)) — ensure restoration runs
even if the test panics by using a guard/RAII pattern or a finally-style drop
guard inside test_object_exists_404_does_not_retry so the global retries are
returned to their original value.

@durch
Copy link
Copy Markdown
Owner Author

durch commented May 4, 2026

Thanks for tightening the object_exists behavior. This makes expected 404 Not Found responses terminal for object_exists instead of sending them through the retry path, while preserving retry/error behavior for the rest of the request flow.

Verification before merge:

  • local make ci passed in the issue worktree
  • refreshed GitHub checks are green after Fix presign tests: use fake credentials instead of minio env vars #453 restored the presign-test baseline
  • reviewed under triage rules: PASS preliminary; this touches retry/status handling across backends, but the scope is targeted and the regression test covers the expected no-retry 404 behavior

Merging this as the fix for #444.

@durch durch merged commit 3ce16e8 into master May 4, 2026
6 checks passed
@durch durch deleted the fix/issue-444-object-exists-404 branch May 4, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

retry! macro in response_data triggers unnecessary retry for object_exists

1 participant