Skip to content

retry: false still performs one retry after the initial failed fetch #272

Description

@MicroMilo

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:

{
  "fetchCalls": 1
}

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions