Add automatic HTTP retries with exponential backoff#213
Conversation
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
| } | ||
|
|
||
| return this.handler({ uri: newURI, body, method, headers: redirectHeaders }); | ||
| return this.execute({ uri: newURI, body, method, headers: redirectHeaders }); |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 61b19c4. Configure here.


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 withX-Retry-Count, and caps total backoff per call. Default 3 retries based oncustomerio-analytics-node, configurable/disable via aretryclient option. Defaults:200ms..5swindow,30stotal 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
CIORequestlayer used byTrackClient,APIClient, andPipelinesClient. Transient failures—network/fetcherrors, timeouts, and selected 5xx/429-style statuses—are retried with exponential backoff, jitter, optionalRetry-After, per-attemptX-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 newretryclient option;RetryOptionsis 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-Afterand Pipelines retry idempotency (same body/messageIdacross attempts).Reviewed by Cursor Bugbot for commit 61b19c4. Bugbot is set up for automated code reviews on this repo. Configure here.