Skip to content

feat: retry transient 5xx errors in withSpinner#242

Draft
bukinoshita wants to merge 1 commit intomainfrom
fix/retry-transient-5xx-43c8
Draft

feat: retry transient 5xx errors in withSpinner#242
bukinoshita wants to merge 1 commit intomainfrom
fix/retry-transient-5xx-43c8

Conversation

@bukinoshita
Copy link
Copy Markdown
Member

@bukinoshita bukinoshita commented Apr 9, 2026

Summary

Resolves BU-639: Template automation exits on transient Resend 5xx failures.

The withSpinner() retry wrapper previously only retried rate_limit_exceeded (HTTP 429) errors. Transient server-side 5xx responses (internal_server_error, service_unavailable, gateway_timeout) caused immediate command failure — problematic for CI jobs and agent-driven template management.

Changes

  • src/lib/is-transient-error.ts — New utility predicate that identifies transient 5xx-class error names from the Resend SDK.
  • src/lib/spinner.ts — Changed withSpinner first parameter from a plain string to a SpinnerMessages object with an optional retryTransient flag. When enabled, transient errors are retried with the same exponential backoff policy as rate limits.
  • src/lib/actions.ts — Enabled retryTransient by default for safe read operations (runGet, runList). Added opt-in retryTransient field to runWrite config. Left runCreate and runDelete without transient retries to avoid duplicate side effects on non-idempotent operations.
  • src/commands/templates/update.ts and src/commands/templates/publish.ts — Opted into transient retries since these are idempotent write operations.
  • Direct withSpinner callers (emails/send.ts, emails/batch.ts, emails/receiving/attachment.ts, contacts/update-topics.ts) — Updated to use the new object signature. Attachment fetch gets retryTransient: true (read-only); send/batch/update-topics do not (non-idempotent).

Testing

  • Added tests/lib/is-transient-error.test.ts with 6 tests covering all transient names plus negative cases.
  • Added withSpinner retry on transient 5xx errors describe block in tests/lib/spinner.test.ts with 6 tests:
    • Retries each transient error name when retryTransient: true
    • Does not retry transient errors when retryTransient is not set
    • Exhausts max retries and errors on persistent transient failures
    • Still retries rate_limit_exceeded regardless of retryTransient flag
  • All 747 tests pass, lint and typecheck clean.

Linear Issue: BU-639

Open in Web Open in Cursor 

Summary by cubic

Add opt-in retries for transient 5xx errors in withSpinner to make read paths and idempotent template writes resilient to brief outages. Addresses BU-639 by preventing failures on internal_server_error, service_unavailable, and gateway_timeout.

  • New Features

    • withSpinner now accepts { loading, retryTransient? }; always retries rate_limit_exceeded, and when retryTransient is true, also retries transient 5xx with the same backoff/retry-after logic.
    • Default retryTransient: true for runGet and runList; optional on runWrite (enabled for templates:update and templates:publish; left off for create, delete, emails:send, emails:batch; enabled for read-only emails:receiving:attachment).
    • Added isTransientError utility.
  • Migration

    • Replace withSpinner('message', ...) with withSpinner({ loading: 'message' }, ...).
    • For idempotent or read-only operations, set retryTransient: true; keep it off for non-idempotent writes.
    • No changes needed where runGet/runList are used (they now retry transient errors by default).

Written for commit 1628223. Summary will update on new commits.

Add opt-in retry support for transient server-side errors
(internal_server_error, service_unavailable, gateway_timeout) in the
withSpinner retry loop, alongside the existing rate_limit_exceeded
handling.

- Extract isTransientError predicate into src/lib/is-transient-error.ts
- Change withSpinner first param from string to SpinnerMessages object
  with optional retryTransient flag
- Enable retryTransient for safe read operations (runGet, runList) and
  idempotent writes (template update, template publish)
- Keep retryTransient disabled for non-idempotent commands (create,
  delete, send, batch) to avoid duplicate side effects
- Update all direct withSpinner callers to use the new object signature
- Add tests for transient retry behavior and isTransientError predicate

Co-authored-by: Bu Kinoshita <bukinoshita@users.noreply.github.com>
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