Skip to content

feat(web): 429/Retry-After handling in API client (TASK-2026)#882

Merged
xarmian merged 3 commits into
mainfrom
feat/task-2026-429-retry
Jul 9, 2026
Merged

feat(web): 429/Retry-After handling in API client (TASK-2026)#882
xarmian merged 3 commits into
mainfrom
feat/task-2026-429-retry

Conversation

@xarmian

@xarmian xarmian commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

What

Adds client-side 429 / rate-limit handling to the web API request wrapper (web/src/lib/api/client.ts), from the PLAN-1984 Web-UX audit. Previously the client had zero 429 handling despite rate limits being hit fairly consistently.

Retry policy

  • Retry-After parsing — supports both wire forms: delta-seconds (Retry-After: 3) and HTTP-date (Retry-After: Wed, 21 Oct 2026 07:28:00 GMT). Clamped to a 5s ceiling so a hostile/misconfigured huge value can't hang the UI; missing/garbage headers fall back to a 750ms default backoff.
  • One delayed retry, GET/HEAD only — idempotent requests get a single Retry-After-delayed retry. POST/PATCH/DELETE never auto-retry (avoids duplicate writes).
  • Distinct error — an exhausted retry (or any non-idempotent 429) surfaces PadApiError with code: 'rate_limited', a "Server busy — please try again in a moment." message, and a retryAfterMs field. New isRateLimitError() helper (exported) mirrors the isPlanLimitError idiom for UI branching.
  • Backoff-aware bootstrap / global cooldown — every observed 429 arms a monotonic global cooldown (now + Retry-After, pushed forward only). Subsequent idempotent GETs wait out the remaining window (bounded to 5s total) before firing, so independent call sites don't burst a busy server — e.g. localIndex.bootstrap's reconcile 429s, then the collection page immediately fires a follow-up deltaSync. The cooldown expires by wall-clock (not cleared on an unrelated in-flight success, which isn't evidence the limit lifted).

Non-429 code paths are unchanged; the cooldown is inert unless a 429 was recently observed.

Test plan

Focused vitest coverage in client.test.ts: Retry-After parse (seconds / HTTP-date / past-date / missing / garbage / clamp), GET retries exactly once, second 429 surfaces rate_limited, POST does NOT retry, retryAfterMs carried on the error, and the global cooldown makes a follow-up GET wait out the last Retry-After. npm run check (0 errors), npm run build, and npm run test (138 passing) all green. Two rounds of Codex review — CLEAN.

Refs

TASK-2026, PLAN-1984. Complements IDEA-1842 (the separate server-side rate-limit raise) — this is the client-side handling only and does not duplicate it.

@xarmian xarmian merged commit d58698a into main Jul 9, 2026
3 of 4 checks passed
@xarmian xarmian deleted the feat/task-2026-429-retry branch July 9, 2026 19: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.

1 participant