Skip to content

Add automatic HTTP retries with exponential backoff#213

Merged
mike-engel merged 1 commit into
feature/fetch-swapfrom
cdp-6039
Jun 4, 2026
Merged

Add automatic HTTP retries with exponential backoff#213
mike-engel merged 1 commit into
feature/fetch-swapfrom
cdp-6039

Conversation

@mike-engel
Copy link
Copy Markdown
Collaborator

@mike-engel mike-engel commented Jun 4, 2026

Adds a stateless retry layer in CIORequest.handler() shared by TrackClient, APIClient, and PipelinesClient. Retries transient network errors and the retryable status codes (408/429/500/502/503/504/522/524) with exponential backoff + jitter, honors Retry-After, tags attempts with X-Retry-Count, and caps total backoff per call. Default 3 retries based on customerio-analytics-node, configurable/disable via a retry client option. Defaults: 200ms..5s window, 30s total budget.

Also disables retries in the fixture-replay harness (one mock per attempt) and cancels pending delayed nock responses after each replay test to fix a pre-existing uncaught InterceptorError from the timeout fixture.


Note

Medium Risk
All outbound API traffic now may sleep and replay up to several times by default, which changes latency and duplicate-delivery exposure on non-idempotent calls unless callers opt out via maxRetries: 0.

Overview
Adds automatic HTTP retries in the shared CIORequest layer used by TrackClient, APIClient, and PipelinesClient. Transient failures—network/fetch errors, timeouts, and selected 5xx/429-style statuses—are retried with exponential backoff, jitter, optional Retry-After, per-attempt X-Retry-Count, and a total backoff budget; other 4xx and deterministic errors (e.g. bad JSON) fail immediately. Policy defaults to 3 retries and is tunable or disabled via a new retry client option; RetryOptions is exported from the package entry.

Documents the behavior in README and CHANGELOG. Fixture replay turns retries off by default (one mock per call) and aborts pending delayed nock responses after each test. New tests cover retry/backoff/Retry-After and Pipelines retry idempotency (same body/messageId across attempts).

Reviewed by Cursor Bugbot for commit 61b19c4. Bugbot is set up for automated code reviews on this repo. Configure here.

@mike-engel mike-engel self-assigned this Jun 4, 2026
Adds a stateless retry layer in CIORequest.handler() shared by TrackClient,
APIClient, and PipelinesClient. Retries transient network errors and the
retryable status codes (408/429/500/502/503/504/522/524) with exponential
backoff + jitter, honors Retry-After, tags attempts with X-Retry-Count, and
caps total backoff per call. Default 3 retries; configurable/disable via a
`retry` client option. Defaults: 200ms..5s window, 30s total budget.

Also disables retries in the fixture-replay harness (one mock per attempt) and
cancels pending delayed nock responses after each replay test to fix a
pre-existing uncaught InterceptorError from the timeout fixture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 61b19c4. Configure here.

Comment thread lib/request.ts
}

return this.handler({ uri: newURI, body, method, headers: redirectHeaders });
return this.execute({ uri: newURI, body, method, headers: redirectHeaders });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-JSON error bodies bypass retry for retryable statuses

Medium Severity

When execute() receives a retryable status code (e.g. 500, 502, 503) with a non-JSON body (such as an HTML error page from a CDN or load balancer), the JSON parse failure at line 168 throws a plain Error before the status-code check at line 171. isRetryable() only recognizes CustomerIORequestError, TypeError, and DOMException, so this plain Error is classified as non-retryable and the request is never retried. This contradicts the documented guarantee that all retryStatusCodes are retried.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 61b19c4. Configure here.

@mike-engel mike-engel merged commit 491c0f0 into feature/fetch-swap Jun 4, 2026
11 checks passed
@mike-engel mike-engel deleted the cdp-6039 branch June 4, 2026 19:11
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