Summary
When HttpClient is configured with retry: false, a failed request is expected to perform only the initial fetch attempt. Instead, the current retry normalization appears to allow one extra retry, so a failing request calls fetch twice.
Steps to reproduce
From the repository root, run a small repro with fetch monkeypatched to fail and count calls:
const { HttpClient } = await import("./src/client/http.ts")
let fetchCalls = 0
const originalFetch = globalThis.fetch
globalThis.fetch = async () => {
fetchCalls += 1
throw new Error(`forced network failure ${fetchCalls}`)
}
const client = new HttpClient({
baseUrl: "https://example.com",
authorization: "Bearer test-token",
retry: false,
devMode: false,
})
let error
try {
await client.request({
method: "GET",
path: ["v2", "messages", "msg_123"],
})
} catch (caught) {
error = caught instanceof Error ? caught.message : String(caught)
} finally {
globalThis.fetch = originalFetch
}
console.log({ fetchCalls, error })
Expected behavior
fetch should be called once, because retry: false should disable retries.
Expected result:
Actual behavior
fetch is called twice. The surfaced error is from the second forced failure:
{
"fetchCalls": 2,
"error": "forced network failure 2"
}
Evidence
Relevant source anchors:
- Public client config exposes retry behavior.
RetryConfig allows the false branch.
- Object-form
retries is documented as the number of retries before giving up.
retry: false is normalized to attempts: 1.
- The request loop runs with
index <= this.retry.attempts.
- When
index < this.retry.attempts, the client sleeps and retries.
This means attempts: 1 is consumed as one retry after the initial attempt, not as one total attempt.
Suggested fix
Use one consistent internal meaning for the retry counter. The smallest fix seems to be normalizing retry: false to attempts: 0 while keeping the current <= loop semantics for object-form retry counts.
Suggested tests:
retry: false with failing fetch should call fetch exactly once.
{ retries: 1 } should still call fetch twice, preserving current object-form semantics.
Additional context / Related coverage
A current GitHub search did not identify existing issue or PR coverage for this exact behavior or root cause. The current retry tests appear to cover custom retry timing, but not the disabled-retry case.
Submitted with Codex.
Summary
When
HttpClientis configured withretry: false, a failed request is expected to perform only the initialfetchattempt. Instead, the current retry normalization appears to allow one extra retry, so a failing request callsfetchtwice.Steps to reproduce
From the repository root, run a small repro with
fetchmonkeypatched to fail and count calls:Expected behavior
fetchshould be called once, becauseretry: falseshould disable retries.Expected result:
{ "fetchCalls": 1 }Actual behavior
fetchis called twice. The surfaced error is from the second forced failure:{ "fetchCalls": 2, "error": "forced network failure 2" }Evidence
Relevant source anchors:
RetryConfigallows thefalsebranch.retriesis documented as the number of retries before giving up.retry: falseis normalized toattempts: 1.index <= this.retry.attempts.index < this.retry.attempts, the client sleeps and retries.This means
attempts: 1is consumed as one retry after the initial attempt, not as one total attempt.Suggested fix
Use one consistent internal meaning for the retry counter. The smallest fix seems to be normalizing
retry: falsetoattempts: 0while keeping the current<=loop semantics for object-form retry counts.Suggested tests:
retry: falsewith failingfetchshould callfetchexactly once.{ retries: 1 }should still callfetchtwice, preserving current object-form semantics.Additional context / Related coverage
A current GitHub search did not identify existing issue or PR coverage for this exact behavior or root cause. The current retry tests appear to cover custom retry timing, but not the disabled-retry case.
Submitted with Codex.