Skip to content

fix(utils): clamp retry delays at the timer boundary - #3704

Open
sj0618 wants to merge 1 commit into
Yeachan-Heo:devfrom
sj0618:codex/fix-retry-delay-overflow-v3
Open

fix(utils): clamp retry delays at the timer boundary#3704
sj0618 wants to merge 1 commit into
Yeachan-Heo:devfrom
sj0618:codex/fix-retry-delay-overflow-v3

Conversation

@sj0618

@sj0618 sj0618 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

What

  • Normalize caller-provided retry delays immediately before both scheduler boundaries.
  • Clamp response, network-error, and server-hint delays to 2_147_483_647 ms so Node/Bun timers cannot overflow into an immediate retry.
  • Preserve retry counts, server-hint fail-fast behavior, response-body cancellation, and abort handling.

Why

This is the current-dev successor to #3672 and the corrected successor to #3612. #3672's product checks were green, but it was closed on a stale base and inherited integration guard. The owner explicitly requested a current-dev successor preserving the timer ceiling and deterministic overflow/server-hint coverage.

The original contributor attribution is preserved in the commit.

Testing

  • bun test packages/utils/test/fetch-retry.test.ts — 39 pass, 110 assertions
  • bun --cwd=packages/utils run check
  • git diff --check upstream/dev...HEAD

GJC verdict

gajae.pr-review-verdict.v1 merge-approved sha256:ccad8ed1f82ccab0b5675aaebd26444895e1c095 reviewer:critic evidence:local-exact-head-review

  • Target branch is dev
  • bun check passes
  • Tested locally
  • CHANGELOG updated (if user-facing)
  • Verdict above matches the exact PR head, not an earlier commit

Normalize caller-provided retry delays immediately before both scheduler boundaries and cap them at the signed 32-bit timer ceiling.

Successor to Yeachan-Heo#3672; preserves the original contributor work and maintainer-requested overflow coverage.

Co-authored-by: Oreochococukie <102867217+Oreochococukie@users.noreply.github.com>

Co-authored-by: Gajae Code Contributor <contributor@users.noreply.github.com>

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Exact-head red-team review for ccad8ed.

Verdict: MERGE_READY (code)

Change summary

packages/utils/src/fetch-retry.ts introduces normalizeRetryDelay(delayMs, maxDelayMs) which caps every scheduled retry delay at min(delayMs, maxDelayMs, MAX_TIMER_DELAY_MS) and returns 0 for negative/NaN/Infinity-after-cap values. This prevents large exponential backoffs or server Retry-After hints from overflowing the signed 32-bit timer ceiling (2^31-1 ms), which Node/Bun coerce to 1ms — producing an immediate retry instead of the intended long delay.

Red-team

  • MAX_TIMER_DELAY_MS = 2_147_483_647 (2^31-1): correct for signed 32-bit. Bun and Node both use this boundary.
  • normalizeRetryDelay clamps to [0, cap]: NaN → 0, negative → 0, +Infinity → maxDelayMs (typically 60_000). Correct.
  • resolveDefaultDelay no longer applies maxDelayMs internally — capping is centralized in normalizeRetryDelay. Clean separation.
  • All three code paths (default delay, server hint, network error) now route through normalizeRetryDelay. Complete.
  • 20 new test cases covering negative, NaN, Infinity, timer-ceiling overflow, server hints, and both response/network error paths. Thorough.

Verification

  • bun test fetch-retry.test.ts — 39 pass, 110 assertions
  • biome check — clean
  • tsc --noEmit — no errors
  • git merge-tree vs dev — 0 conflict markers (auto-merges cleanly; DIRTY is stale base)
  • CI — 16 pass, 5 skip

Mergeability note: GitHub reports CONFLICTING/DIRTY (stale base b40bc27). Confirmed auto-merges cleanly against current dev 57577ac. Rebase to refresh.

Review receipt: gajae.pr-review-verdict.v1 merge-ready sha256:ccad8ed1f82ccab0b5675aaebd26444895e1c095 reviewer:Yeachan-Heo evidence:terminal-exact-head-red-team

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Exact-head GPT-heavy adversarial batch review for ccad8ed1f82ccab0b5675aaebd26444895e1c095.

Recorded PR base: b40bc271502a0802e212e5538c5d0e8436643029. Current dev: 732856b3ccb3fade6e9fbc17908a4fbca5a7682f (78 commits ahead).

Terminal verdict: REQUEST_CHANGES

Code review result

I found no timer-arithmetic or cancellation defect in the exact source change:

  • Both scheduler boundaries normalize through Math.min(delayMs, maxDelayMs, 2_147_483_647) and map negative/NaN post-cap values to zero.
  • Huge finite values, +Infinity, response delays, network-error delays, and server hints are capped before scheduler.wait.
  • Server-hint fail-fast ordering, retry counts, fire-and-forget response-body cancellation, returned-body ownership, scheduler rejection, and abort-signal propagation remain intact.
  • Local exact-head test: bun test packages/utils/test/fetch-retry.test.ts39 pass, 0 fail.
  • Real Bun probes for a 2^31 configured delay and a 2^31 Retry-After hint stayed on attempt 1 until AbortError at ~26 ms rather than overflowing into an immediate retry. An implicit exponential schedule reached tail delays [1048576000,2097152000,2147483647].

Non-blocking coverage gap: the committed regression matrix does not exercise the implicit defaultDelayMs === undefined exponential overflow branch; it only supplies explicit oversized values/hints.

Blocking current-base state

GitHub currently reports mergeable=false, mergeable_state=dirty, and rebaseable=false. Current dev has not changed fetch-retry.ts or its test since the recorded base, but it has changed packages/utils/CHANGELOG.md; the current-base merge has a real overlap there. This exact head therefore cannot merge.

The Aug 1 automation is green for the stale head/base (16 passing and 5 skipped checks, including the targeted fetch-retry test), and the prior owner approval is attached to this exact head, but that review assessed an older dev (57577acd...). Neither is evidence for a resolved merge result on current dev 732856b3.... Resolve/rebase the changelog against current dev; the resulting new head needs current-base CI and a new exact-head terminal review.

Automated feedback: no issue comments, inline review comments, or review threads; the only submitted review before this one was the prior exact-head owner approval.

Signed terminal receipt: gajae.pr-review-verdict.v1 request-changes sha256:ccad8ed1f82ccab0b5675aaebd26444895e1c095 reviewer:Yeachan-Heo evidence:gpt-heavy-adversarial-batch-2026-08-05-current-dev-conflict

@Yeachan-Heo

Copy link
Copy Markdown
Owner

Triage note (maintainer batch, 2026-08-05): the clamp is code-verified (both prior red-team reviews found no timer-arithmetic or cancellation defect) and all checks are green on head ccad8ed1. git merge of this head into current dev (f359a9d7e) completes cleanly — the CHANGELOG overlap that the last adversarial review flagged against 732856b3 no longer conflicts on current dev. However, GitHub's mergeability cache is stuck on mergeable=false / mergeable_state=dirty and both merge endpoints refuse. This head was never rebased onto current dev (recorded base b40bc271), so please push a rebase onto current dev to force GitHub to recompute mergeability, then request re-review. No code changes needed.

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Verdict: Request changes — the code change is sound, but the PR cannot merge into current dev and its changelog entry would land in a released section.

Blocker: changelog entry + stale base (merge-blocking).

  • GitHub computes this PR as mergeable: false / mergeable_state: "dirty" against current dev (0fdbb5e80). The base branch has advanced well past the PR's b40bc2715 base: dev released 0.12.8 (2026-08-02) and 0.12.10/0.12.11 (2026-08-03), which folded the PR's anchor line (the #3696 macOS entry) into the released ## [0.12.8] section.
  • Simulated 3-way merge of ccad8ed into current dev: the fetchWithRetry changelog entry lands inside the released ## [0.12.8] - 2026-08-02### Fixed block (after the released macOS and postmortem entries), leaving the current empty ## [Unreleased] section untouched. Per the repo changelog contract, entries must go under ## [Unreleased] and released sections are never edited; this would also misattribute the fix to 0.12.8 in the release notes.
  • Required fix: merge/rebase current dev into the PR branch and move the entry to the top ## [Unreleased] section, then re-push so the dirty state clears. No code changes are needed.

Verified solid (no changes requested there):

  • The fix is correct: resolveDefaultDelay returns raw values and normalizeRetryDelay caps every scheduled delay at min(delay, maxDelayMs, 2_147_483_647) at both scheduler boundaries, mapping negative/non-finite to 0. The premise is real — Bun.sleep/node:timers/promises wait coerces delays above the signed 32-bit ceiling to ~1ms (retry storm); the cap removes that.
  • Behavior preserved for all well-formed inputs (finite, non-negative, ≤ cap); only previously-pathological values (negative/NaN/±Infinity/overflow) change, to a sane 0 or capped delay. The fail-fast hint > maxDelayMs contract is unchanged.
  • Tests: 39/39 pass at head (24 pre-existing + 15 new); the new cases genuinely catch the bug — 21 assertions fail against the old implementation (verified) and pass with the new one.
  • CI at head is green: 21/21 check runs (success or path-based skip), including the targeted fetch-retry.test.ts job and gjc-state-gates.
  • No generated artifacts touched; private helpers only; no new dependencies; no security/privacy surface change (capping hostile Retry-After sleeps is a hardening).

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.

2 participants