axios-retryer
Version:
TypeScript-first Axios retry library with concurrency limits, request priority, token refresh, response caching, and circuit breaker plugins.
1,358 lines (1,331 loc) • 54 kB
TypeScript
import { Method, AxiosRequestConfig, AxiosError, AxiosInstance, AxiosResponse } from 'axios';
type AxiosRetryerHttpMethod = Method;
declare const AXIOS_RETRYER_REQUEST_PRIORITIES: {
readonly CRITICAL: 4;
readonly HIGHEST: 3;
readonly HIGH: 2;
readonly MEDIUM: 1;
readonly LOW: 0;
};
type AxiosRetryerRequestPriority = (typeof AXIOS_RETRYER_REQUEST_PRIORITIES)[keyof typeof AXIOS_RETRYER_REQUEST_PRIORITIES];
type RetryEventArgs<TEvents extends object, K extends keyof TEvents> = NonNullable<TEvents[K]> extends (...args: infer TArgs) => unknown ? TArgs : never;
type RetryEventListener<TEvents extends object, K extends keyof TEvents> = (...args: RetryEventArgs<TEvents, K>) => void;
/**
* Terminal request error payload emitted by `onRequestError`.
*/
interface AxiosRetryerRequestErrorEvent {
/** Final Axios error object that caused request failure. */
error: AxiosError;
/** Final Axios request config that failed. */
config: AxiosRequestConfig;
/** HTTP status if available, otherwise `null` for network-level failures. */
status: number | null;
/** Request identifier if available. */
requestId?: string;
/** Total attempts performed including the initial attempt. */
attempts: number;
/** Whether the final error shape is considered retryable by the active strategy. */
retryable: boolean;
}
/**
* Queue-entry payload emitted by `onRequestQueued`.
*/
interface AxiosRetryerRequestQueuedEvent {
/** Request identifier generated or assigned by RetryManager. */
requestId: string;
/** Request config entering the queue. */
config: AxiosRequestConfig;
/** Resolved priority used for queue ordering. */
priority: AxiosRetryerRequestPriority;
/** Queue size immediately after this request was enqueued. */
queueSize: number;
}
/**
* Queue-dispatch payload emitted by `onRequestDispatched`.
*/
interface AxiosRetryerRequestDispatchedEvent {
/** Request identifier generated or assigned by RetryManager. */
requestId: string;
/** Request config dispatched from the queue. */
config: AxiosRequestConfig;
/** Resolved priority used for queue ordering. */
priority: AxiosRetryerRequestPriority;
/** Time spent waiting in the queue before dispatch (milliseconds). */
queuedForMs: number;
}
/**
* Success payload emitted by `onRequestSucceeded`.
*/
interface AxiosRetryerRequestSucceededEvent {
/** Request identifier generated or assigned by RetryManager. */
requestId?: string;
/** Final request config that succeeded. */
config: AxiosRequestConfig;
/** Final HTTP status code. */
status: number;
/** Total attempts performed including the initial attempt. */
attempts: number;
}
/**
* Core events exposed by RetryManager without any plugins attached.
*/
interface CoreRetryEvents {
/**
* Triggered when the retry process begins.
*/
onRetryProcessStarted?: () => void;
/**
* Triggered before each retry attempt.
* @param config The Axios request configuration being retried.
*/
beforeRetry?: (config: AxiosRequestConfig) => void;
/**
* Triggered after a retry attempt.
* @param config The Axios request configuration being retried.
* @param success Whether the retry was successful.
* @param error If the retry failed, the error that caused the failure.
*/
afterRetry?: (config: AxiosRequestConfig, success: boolean, error?: AxiosError) => void;
/**
* Triggered when a retry is scheduled and waiting for the specified delay.
* @param delayMs The delay in milliseconds.
* @param config The Axios request configuration.
*/
onRetryScheduled?: (delayMs: number, config: AxiosRequestConfig) => void;
/**
* Triggered for each failed retry attempt.
* @param config The failed Axios request configuration.
*/
onFailure?: (config: AxiosRequestConfig) => void;
/**
* Triggered when a request enters the queue.
*
* @param payload Queue entry metadata for this request.
*/
onRequestQueued?: (payload: AxiosRetryerRequestQueuedEvent) => void;
/**
* Triggered when a queued request is dispatched from the queue to the network layer.
*
* @param payload Dispatch metadata including queue wait duration.
*/
onRequestDispatched?: (payload: AxiosRetryerRequestDispatchedEvent) => void;
/**
* Triggered when a request succeeds (initial attempt or after retries).
*
* @param payload Success metadata for this request.
*/
onRequestSucceeded?: (payload: AxiosRetryerRequestSucceededEvent) => void;
/**
* Triggered once when a request fails terminally (all retries exhausted or no-retry terminal path).
* Unlike `onFailure`, this event is emitted only for the final failure.
*
* @param payload Terminal error context for application-level handling.
*/
onRequestError?: (payload: AxiosRetryerRequestErrorEvent) => void;
/**
* Triggered when all retries are completed.
*/
onRetryProcessFinished?: () => void;
/**
* Triggered when an in-flight retry delay timer is cancelled — either because
* the user aborted the request (`source: 'user'`) or because the system shut
* the request down (`source: 'system'`, e.g. plugin destroy, queue clear).
*/
onRetryTimerCancelled?: (payload: {
requestId: string;
source: 'user' | 'system';
}) => void;
/**
* Triggered when a request cancelled.
* @param requestId Id of the cancelled request.
*/
onRequestCancelled?: (requestId: string) => void;
/**
* Called when a request fails due to network or connection issues, meaning
* no valid server response was received (e.g., user is offline).
*
* @param request - The Axios request config that encountered a connection error.
*/
onInternetConnectionError?: (request: AxiosRequestConfig) => void;
/**
* Triggered when a blocking request (at or above `blockingPriorityThreshold`) fails terminally.
* Only fires when `blockingPriorityThreshold` is configured.
*
* @param config The Axios request config of the failed blocking request.
*/
onBlockingRequestFailed?: (config: AxiosRequestConfig) => void;
/**
* Triggered when every in-flight blocking request (at or above `blockingPriorityThreshold`)
* has **succeeded** (terminal success) and none remain in the internal blocker set.
* Not emitted when a blocker fails (`onBlockingRequestFailed`) or is cancelled.
* Only fires when `blockingPriorityThreshold` is configured.
*/
onAllBlockingRequestsResolved?: () => void;
}
type RetryManagerEvents<TPluginEvents extends object = Record<never, never>> = {
[K in keyof CoreRetryEvents | keyof TPluginEvents]: K extends keyof TPluginEvents ? K extends keyof CoreRetryEvents ? CoreRetryEvents[K] & TPluginEvents[K] : TPluginEvents[K] : K extends keyof CoreRetryEvents ? CoreRetryEvents[K] : never;
};
/**
* AxiosRetryer metrics
* */
interface AxiosRetryerMetrics {
totalRequests: number;
successfulRetries: number;
failedRetries: number;
completelyFailedRequests: number;
canceledRequests: number;
completelyFailedCriticalRequests: number;
errorTypes: {
network: number;
server5xx: number;
client4xx: number;
cancelled: number;
};
retryAttemptsDistribution: Record<string, number>;
requestCountsByPriority: Record<string, number>;
retryPrioritiesDistribution: Record<string, {
total: number;
successes: number;
failures: number;
}>;
queueWaitDuration: number;
retryDelayDuration: number;
}
/**
* Represents the distribution of different error types encountered
*/
interface ErrorTypesDistribution {
/** Number of network-related errors (e.g., connection failures) */
network: number;
/** Number of 5xx server errors */
server5xx: number;
/** Number of 4xx client errors */
client4xx: number;
/** Number of canceled requests */
cancelled: number;
}
/**
* Represents metrics for a specific request priority level
*/
interface PriorityMetrics {
/** The priority level (higher numbers indicate higher priority) */
priority: number;
/** Total number of retry attempts for this priority */
total: number;
/** Number of successful retries for this priority */
successes: number;
/** Number of failed retries for this priority */
failures: number;
/** Success rate percentage for this priority (0-100) */
successRate: number;
/** Failure rate percentage for this priority (0-100) */
failureRate: number;
}
/**
* AxiosRetryer detailed metrics
* */
interface AxiosRetryerDetailedMetrics {
/** Total number of requests made through the retryer */
totalRequests: number;
/** Number of successfully completed retries */
successfulRetries: number;
/** Number of failed retry attempts */
failedRetries: number;
/** Requests that failed all retry attempts */
completelyFailedRequests: number;
/** Requests canceled before completion */
canceledRequests: number;
/** Critical priority requests that failed all retries */
completelyFailedCriticalRequests: number;
/** Distribution of error types encountered */
errorTypesDistribution: ErrorTypesDistribution;
/** Distribution of retry attempts across all requests */
retryAttemptsDistribution: Record<number, number>;
/** Count of requests by priority level */
requestCountsByPriority: Record<number, number>;
/** Average time spent in queue (seconds) */
avgQueueWait: number;
/** Average delay between retry attempts (seconds) */
avgRetryDelay: number;
/** Detailed metrics grouped by request priority */
priorityMetrics: PriorityMetrics[];
/** Timer health and accumulation metrics */
timerHealth: {
/** Number of active internal timers */
activeTimers: number;
/** Number of active retry timers */
activeRetryTimers: number;
/** Health score (0 = excellent, 100+ = potential issues) */
healthScore: number;
};
}
/**
* Interface for pluggable metrics recording.
* The core library ships with no-op metrics by default.
* Use MetricsPlugin for full metrics collection.
*/
interface MetricsRecorder {
reset(): void;
buildDetailedMetrics(timerStats: {
activeTimers: number;
activeRetryTimers: number;
}): AxiosRetryerDetailedMetrics;
emitMetricsUpdated?(): void;
}
/**
* Logger interface used by RetryManager and its collaborators.
* Supply a custom implementation via {@link RetryManagerOptions.logger}
* to redirect or suppress log output.
*/
interface Logger {
log(message: string, data?: unknown): void;
error(message: string, error?: unknown): void;
warn(message: string, data?: unknown): void;
debug(message: string, meta?: unknown): void;
}
/**
* By implementing this interface, we can write our own custom request store
* */
interface RequestStore {
/**
* Add a request config to the store
* */
add(request: AxiosRequestConfig): void;
/**
* Remove a request config to the store
* */
remove(request: AxiosRequestConfig): void;
/**
* Get all request configs from the store
* */
getAll(): AxiosRequestConfig[];
/**
* Clear request store
* */
clear(): void;
}
/**
* Context object passed to plugins during initialization and teardown.
* Provides the plugin-facing view of RetryManager capabilities including
* plugin-only wiring hooks that are not part of the public manager API.
*/
interface PluginContext<TPluginEvents extends object = Record<never, never>> {
/** The Axios instance managed by RetryManager. */
readonly axiosInstance: AxiosInstance;
/** Returns the configured logger. */
getLogger(): Logger;
/** Subscribe to a manager or plugin event. */
on<K extends keyof RetryManagerEvents<TPluginEvents>>(event: K, listener: RetryEventListener<RetryManagerEvents<TPluginEvents>, K>): void;
/** Unsubscribe from a manager or plugin event. */
off<K extends keyof RetryManagerEvents<TPluginEvents>>(event: K, listener: RetryEventListener<RetryManagerEvents<TPluginEvents>, K>): boolean;
/** Fire all listeners registered for this event. */
emit<K extends keyof RetryManagerEvents<TPluginEvents>>(event: K, ...args: RetryEventArgs<RetryManagerEvents<TPluginEvents>, K>): void;
/**
* Identical to `emit` — fires all listeners registered for this event.
*
* Kept for backward compatibility with existing plugins. There is no semantic
* distinction from `emit`: prefer `emit` in new code.
*/
triggerAndEmit<K extends keyof RetryManagerEvents<TPluginEvents>>(event: K, ...args: RetryEventArgs<RetryManagerEvents<TPluginEvents>, K>): void;
/** Cancel a specific in-flight or queued request by its ID. */
cancelRequest(requestId: string): void;
/** Cancel all active and queued requests. */
cancelAllRequests(): void;
/** Cancel only requests currently waiting in the queue. */
cancelQueuedRequests(): void;
/**
* Register a queue gate that must approve each request before it is dispatched.
* Used by plugins that need to block request processing under certain conditions.
*/
registerQueueGate(name: string, canProcess: (request: AxiosRequestConfig) => boolean): void;
/** Remove a previously registered queue gate. */
unregisterQueueGate(name: string): boolean;
/** Trigger a queue drain pass. Useful after a gate condition changes. */
refreshQueue(): void;
/**
* Register or unregister a metrics recorder.
* Pass `null` to detach. Used by MetricsPlugin to expose metric data to the RetryManager's getMetrics() method.
*/
registerMetricsRecorder(recorder: MetricsRecorder | null): void;
/**
* Return active timer counts.
* Used by MetricsPlugin to populate the timerHealth section of detailed metrics.
*/
getTimerStats(): {
activeTimers: number;
activeRetryTimers: number;
};
/**
* Release lifecycle tracking for a request config and mark its queue slot complete.
* Used by TokenRefreshPlugin when a tracked request is intercepted for token refresh.
*/
releaseRequestTracking(config: AxiosRequestConfig): void;
}
/**
* AxiosRetryer plugin interface that can be attached with {@link RetryManager.use} and removed with {@link RetryManager.unuse}
* */
interface RetryPlugin<TPluginEvents extends object = Record<never, never>> {
/**
* Plugin name. Should be unique
* */
name: string;
/**
* Plugin version (e.g. 1.0.0)
* */
version: string;
/**
* Phantom covariant marker for TypeScript to infer `TPluginEvents` at call sites
* such as `manager.use(plugin)`. Never set this at runtime; implementations may
* simply omit it (it is always `undefined`).
* */
readonly _events?: Readonly<TPluginEvents>;
/**
* Called when the plugin is attached and initialized.
* @param context Plugin context providing manager capabilities and plugin-only wiring hooks.
* */
initialize: (context: PluginContext<TPluginEvents>) => void;
/**
* Called before the plugin is removed.
* @param context Plugin context providing manager capabilities and plugin-only wiring hooks.
* */
onBeforeDestroyed?: (context: PluginContext<TPluginEvents>) => void;
}
type MaybePromise<T> = T | Promise<T>;
/**
* Represents a cached item containing the AxiosResponse and the timestamp it was cached.
*/
interface CachedItem {
response: AxiosResponse<unknown>;
timestamp: number;
ttr?: number;
lastAccessedAt?: number;
}
interface CacheStorageEntry {
readonly key: string;
readonly value: CachedItem;
}
interface CacheStorage {
get(key: string): MaybePromise<CachedItem | undefined>;
set(key: string, value: CachedItem): MaybePromise<void>;
delete(key: string): MaybePromise<void>;
clear(): MaybePromise<void>;
/**
* Returns the adapter's full cache index.
* Cleanup and non-exact invalidation operate on this index.
*/
entries(): MaybePromise<readonly CacheStorageEntry[]>;
}
/**
* Options for the CachingPlugin.
*/
interface CachingPluginOptions {
/**
* Response header names that are stripped before a response is written to the cache.
* This prevents sensitive per-user or session-establishing headers (e.g. `Set-Cookie`)
* from being replayed to different callers who receive a cached response.
*
* Matching is case-insensitive.
*
* @default ['set-cookie']
*/
sensitiveResponseHeaders?: readonly string[];
/**
* If true, include the entire headers object in the cache key.
* @default false
*/
compareHeaders?: boolean;
/**
* Duration (in milliseconds) a cached entry is considered fresh.
* If 0, the cache never expires.
* @default 0
*/
timeToRevalidate?: number;
/**
* HTTP methods to cache. By default, only GET requests are cached.
* @default [AXIOS_RETRYER_HTTP_METHODS.GET]
*/
cacheMethods?: readonly AxiosRetryerHttpMethod[];
/**
* Interval in milliseconds to run cache cleanup.
* If 0, periodic cleanup is disabled.
* @default 0
*/
cleanupInterval?: number;
/**
* Maximum age in milliseconds for cached items.
* Items older than this will be removed during cleanup.
* If 0, items don't expire based on age.
* @default 0
*/
maxAge?: number;
/**
* Maximum number of items to keep in cache.
* If exceeded, oldest items will be removed first.
* If 0, no limit is applied.
* @default 1000
*/
maxItems?: number;
/**
* If true, only requests that are retried will be cached.
* Requests that are not retried will not be cached even if they are cacheable.
* @default false
*/
cacheOnlyRetriedRequests?: boolean;
/**
* Indexed storage backend used for cache entries.
* Custom adapters must implement `entries()` so cleanup and invalidation can
* operate on the adapter's source of truth after restart or across processes.
* Defaults to the built-in in-memory storage.
*/
storage?: CacheStorage;
/**
* If true, concurrent identical cacheable requests share the same in-flight response.
* @default true
*/
dedupeConcurrentRequests?: boolean;
/**
* Allows custom cache key composition from canonical request parts.
* The default builder uses normalized method, URL, params, body, and optional headers.
*/
cacheKeyBuilder?: CacheKeyBuilder;
/**
* When true, requests carrying authentication headers (Authorization, Cookie,
* Proxy-Authorization, X-Auth-Token, X-API-Key) are excluded from caching.
* This prevents cross-principal cache collisions on shared retryer instances.
*
* Set to `false` only for legitimate shared-cache use cases where all
* principals should receive the same cached response.
*
* @default true
*/
skipWhenAuthPresent?: boolean;
/**
* Header names whose values are folded into the cache key, binding each
* cache entry to the identity or context carried by those headers.
*
* Use this instead of (or together with) `skipWhenAuthPresent: false`
* when you need per-principal caching rather than skipping the cache entirely.
*
* Header name matching is case-insensitive.
*
* @default []
*
* @example
* ```ts
* // Cache responses per-user based on their Authorization header:
* new CachingPlugin({ skipWhenAuthPresent: false, varyHeaders: ['Authorization'] });
* ```
*/
varyHeaders?: readonly string[];
/**
* Maximum byte size of a response body (measured via `JSON.stringify` length) that
* will be written to the cache. Responses whose serialized body exceeds this limit
* are served normally but not stored.
*
* Use this to prevent a single large response from consuming excessive memory.
* Set to `0` to disable the check (no limit).
*
* @default 0
*/
maxEntrySize?: number;
}
interface CachingRequestOptions {
cache?: boolean;
ttr?: number;
}
interface CacheKeyBuilderContext {
readonly config: AxiosRequestConfig;
readonly method: string;
readonly normalizedUrl: string;
readonly normalizedParams: string;
readonly normalizedData: string;
readonly normalizedHeaders: string;
}
type CacheKeyBuilder = (context: CacheKeyBuilderContext) => string;
interface CachingPluginEvents {
onCacheHit?: (payload: {
keyFingerprint: string;
config: AxiosRequestConfig;
ageMs: number;
}) => void;
onCacheMiss?: (payload: {
keyFingerprint: string;
config: AxiosRequestConfig;
reason: 'empty' | 'stale';
}) => void;
onCacheInvalidated?: (payload: {
count: number;
matcher: 'all' | 'custom';
}) => void;
}
type CacheInvalidationMatcher = string | RegExp | {
exact: string;
} | {
prefix: string;
};
declare class InMemoryCacheStorage implements CacheStorage {
private readonly storage;
get(key: string): CachedItem | undefined;
set(key: string, value: CachedItem): void;
delete(key: string): void;
clear(): void;
entries(): readonly CacheStorageEntry[];
}
declare class CachingPlugin implements RetryPlugin<CachingPluginEvents> {
name: string;
version: string;
readonly _events?: Readonly<CachingPluginEvents>;
private context;
private interceptorIdReq;
private interceptorIdRes;
private static readonly CACHE_CLEANUP_TIMEOUT_MS;
private static readonly CACHE_CLEANUP_DISABLE_AFTER;
private readonly cache;
private readonly inflight;
private readonly cleanup;
private readonly options;
private readonly storage;
constructor(options?: CachingPluginOptions);
initialize(context: PluginContext<CachingPluginEvents>): void;
onBeforeDestroyed(): void;
private getCacheKeyFingerprint;
/**
* Checks if there is a fresh cached response and handles the request accordingly.
*/
private handleRequest;
/**
* Handles successful responses by caching them when appropriate.
*/
private handleResponseSuccess;
private handleResponseError;
/**
* Generates a unique cache key based on the request configuration.
*/
buildCacheKey(config: AxiosRequestConfig): string;
private runCacheCleanup;
/**
* Manually clears all cache entries.
*/
clearCache(): void | Promise<void>;
/**
* Invalidates cache entries using explicit exact-key, prefix, or RegExp matching.
*
* Plain string input is treated as an exact-key match.
*
* @param matcher The exact key, prefix matcher, or RegExp to match for invalidation
* @returns The number of invalidated cache entries
*/
invalidateCache(matcher: CacheInvalidationMatcher): number | Promise<number>;
/**
* Returns current cache statistics.
*/
getCacheStats(): {
size: number;
oldestItemAge: number;
newestItemAge: number;
averageAge: number;
};
private upsertCacheEntry;
private touchCacheEntry;
private deleteCacheEntry;
private getRequestCachingOptions;
private readCacheEntriesForScan;
private syncLocalCache;
private enforceMaxItemsBeforeUpsert;
private persistCacheTouchIfNeeded;
private invalidateCacheEntries;
private buildCacheKeyContext;
}
declare class InvalidCacheKeyError extends Error {
constructor();
}
declare module 'axios' {
interface AxiosRequestConfig {
__cachingOptions?: CachingRequestOptions;
}
}
/**
* Creates a CachingPlugin instance.
* Functional alternative to using the `new CachingPlugin()` constructor.
*
* The caching plugin stores responses from successful requests and returns them
* for identical requests, reducing network traffic and improving performance.
*
* @param options Configuration options for the CachingPlugin
* @returns A configured CachingPlugin instance
*
* @example
* ```typescript
* import { AXIOS_RETRYER_HTTP_METHODS } from 'axios-retryer';
*
* const cachePlugin = createCachePlugin({
* timeToRevalidate: 60000, // Cache responses for 60 seconds
* cacheMethods: [AXIOS_RETRYER_HTTP_METHODS.GET], // Only cache GET requests
* cleanupInterval: 300000, // Run cleanup every 5 minutes
* maxItems: 100, // Store at most 100 responses
* compareHeaders: false // Don't include headers in cache key
* });
*
* manager.use(cachePlugin);
* ```
*/
declare function createCachePlugin(options?: CachingPluginOptions): CachingPlugin;
declare const CIRCUIT_BREAKER_STATES: {
readonly CLOSED: "CLOSED";
readonly OPEN: "OPEN";
readonly HALF_OPEN: "HALF_OPEN";
};
type CircuitBreakerState = (typeof CIRCUIT_BREAKER_STATES)[keyof typeof CIRCUIT_BREAKER_STATES];
declare const CIRCUIT_BREAKER_SCOPES: {
readonly HOST: "host";
readonly URL: "url";
readonly HOST_AND_URL: "host+url";
};
type CircuitBreakerScope = (typeof CIRCUIT_BREAKER_SCOPES)[keyof typeof CIRCUIT_BREAKER_SCOPES];
/**
* Configuration options for the Circuit Breaker behavior.
*/
interface CircuitBreakerOptions {
/**
* Number of consecutive failures required to trip the circuit.
* Once this threshold is exceeded, the circuit transitions from `CLOSED` to `OPEN`.
*/
failureThreshold: number;
/**
* Duration (in milliseconds) the circuit remains in the `OPEN` state
* before allowing a test request in the `HALF_OPEN` state.
*/
openTimeout: number;
/**
* Maximum number of test requests allowed in `HALF_OPEN` state
* before deciding to either reset (back to `CLOSED`) or trip again to `OPEN`.
*/
halfOpenMax: number;
/**
* Number of successful test requests required in HALF_OPEN state to reset the circuit.
* Must be <= halfOpenMax.
*/
successThreshold?: number;
/**
* If true, uses a sliding window approach to count failures over time rather than consecutive failures.
*/
useSlidingWindow?: boolean;
/**
* The duration (in milliseconds) of the sliding window when useSlidingWindow is true.
*/
slidingWindowSize?: number;
/**
* Callback function to determine which errors should contribute to circuit breaking.
* If not provided, all errors count.
*/
shouldCountError?: (error: AxiosError) => boolean;
/**
* Adaptive timeout configuration. When true, the circuit breaker will track response times
* and adjust timeouts accordingly.
*/
adaptiveTimeout?: boolean;
/**
* Percentile (0-1) to use for adaptive timeout calculation. Default is 0.95 (95th percentile).
*/
adaptiveTimeoutPercentile?: number;
/**
* Number of historical response times to track for adaptive timeout calculation.
*/
adaptiveTimeoutSampleSize?: number;
/**
* Timeout multiplier (e.g., 1.5 = 150% of the calculated percentile).
*/
adaptiveTimeoutMultiplier?: number;
/**
* Maximum number of unique scope keys tracked in the adaptive-timeout response-metrics map.
* Prevents unbounded memory growth. When the cap is reached, the oldest entry is evicted.
* Default: 500.
*/
maxTrackedScopes?: number;
/**
* Allow specific endpoints to be excluded from circuit breaking.
*
* String patterns use exact URL equality. `RegExp` patterns are tested against
* the full request URL on every request — prefer string patterns when possible.
*
* **Security note (ReDoS):** Avoid catastrophically backtracking patterns such as
* `/(a+)+$/` combined with long non-matching URLs. JavaScript's regex engine is
* single-threaded and synchronous; a pathological pattern will block the event loop.
* Validate any user-controlled or externally-sourced patterns before use.
*/
excludeUrls?: readonly (string | RegExp)[];
/**
* Controls how circuit state is grouped.
* `host+url` scopes by host and normalized URL, which is the default.
*/
scope?: CircuitBreakerScope | ((config: AxiosRequestConfig) => string);
/**
* Optional adapter for sharing circuit state across processes or hosts.
*/
stateAdapter?: CircuitBreakerStateAdapter;
}
interface CircuitBreakerFailureRecord {
timestamp: number;
url: string;
status?: number;
errorCode?: string;
}
interface CircuitBreakerScopeState {
state: CircuitBreakerState;
failureCount: number;
successCount: number;
halfOpenCount: number;
nextAttempt: number;
recentFailures: CircuitBreakerFailureRecord[];
lastFailureStatus?: number;
lastFailureCode?: string;
}
interface CircuitBreakerScopeMetrics {
scopeKey: string;
url: string;
host?: string;
state: CircuitBreakerState;
failureCount: number;
halfOpenCount: number;
successCount: number;
nextAttemptIn: number;
failuresInWindow: number;
}
interface CircuitBreakerAdaptiveTimeoutMetrics {
scopeKey: string;
url: string;
host?: string;
timeoutMs: number;
p95ResponseTimeMs: number;
samplesCount: number;
}
interface CircuitBreakerMetrics {
state: CircuitBreakerState;
failureCount: number;
halfOpenCount: number;
successCount: number;
nextAttemptIn: number;
failuresInWindow: number;
adaptiveTimeouts: CircuitBreakerAdaptiveTimeoutMetrics[];
scopeMetrics: CircuitBreakerScopeMetrics[];
}
interface CircuitBreakerStateAdapter {
get(key: string): CircuitBreakerScopeState | undefined | Promise<CircuitBreakerScopeState | undefined>;
set(key: string, state: CircuitBreakerScopeState): void | Promise<void>;
delete(key: string): void | Promise<void>;
clear(): void | Promise<void>;
}
interface CircuitBreakerPluginEvents {
onCircuitStateChanged?: (payload: {
scopeKey: string;
from: CircuitBreakerState;
to: CircuitBreakerState;
reason: 'failure-threshold' | 'half-open-failure' | 'open-timeout-elapsed' | 'success-threshold-reached' | 'manual-reset';
nextAttemptIn?: number;
}) => void;
}
interface ResponseTimeMetrics {
times: number[];
sampleSize: number;
lastCalculated: number;
currentPercentileMs: number;
scopeKey: string;
normalizedUrl: string;
host?: string;
}
/**
* Default in-memory state adapter. Stores circuit state as deep clones to
* prevent accidental mutation of internal state from external references.
*/
declare class InMemoryCircuitBreakerStateAdapter implements CircuitBreakerStateAdapter {
private readonly state;
get(key: string): CircuitBreakerScopeState | undefined;
set(key: string, state: CircuitBreakerScopeState): void;
delete(key: string): void;
clear(): void;
}
/**
* Enhanced CircuitBreakerPlugin with sliding window failure counting,
* selective error monitoring, adaptive timeout management, granular recovery,
* URL exclusion, scoped circuit state, and optional distributed state adapters.
*
* @implements {RetryPlugin}
*/
declare class CircuitBreakerPlugin implements RetryPlugin<CircuitBreakerPluginEvents> {
static STATES: {
readonly CLOSED: "CLOSED";
readonly OPEN: "OPEN";
readonly HALF_OPEN: "HALF_OPEN";
};
readonly name = "CircuitBreakerPlugin";
readonly version = "2.0.0";
readonly _events?: Readonly<CircuitBreakerPluginEvents>;
private readonly _options;
private _requestInterceptorId?;
private _responseInterceptorId?;
private _context;
private readonly _adaptiveTimeoutTracker;
private readonly _scopeManager;
private readonly _failureWindow;
private readonly _metricBaselines;
constructor(options?: Partial<CircuitBreakerOptions>);
/** @internal */
get _responseMetrics(): Record<string, ResponseTimeMetrics>;
/** @internal */
get _scopeStateCache(): Map<string, CircuitBreakerScopeState>;
/** @internal */
get _knownScopes(): Map<string, {
scopeKey: string;
normalizedUrl: string;
host?: string;
}>;
/** @internal */
_normalizeUrl(url: string): string;
/** @internal */
_resolveScopeKey(config: AxiosRequestConfig, normalizedUrl: string, host?: string): string;
/** @internal */
_getScopeDetails(config: AxiosRequestConfig): {
scopeKey: string;
normalizedUrl: string;
host?: string;
};
/** @internal */
_writeScopeState(scopeKey: string, state: CircuitBreakerScopeState): Promise<void>;
initialize(context: PluginContext<CircuitBreakerPluginEvents>): void;
onBeforeDestroyed(context: PluginContext<CircuitBreakerPluginEvents>): void;
getState(scopeKey?: string): CircuitBreakerState;
manualReset(scopeKey?: string): void;
/**
* Backwards-compatible alias retained for existing tests and internal callers.
* @internal
*/
_reset(scopeKey?: string): void;
/**
* @internal Exposed for test inspection only; not part of the public API.
*/
_trackResponseTime(response: AxiosResponse): void;
resetMetrics(): void;
getAdaptiveTimeoutMetrics(): CircuitBreakerMetrics['adaptiveTimeouts'];
getMetrics(): CircuitBreakerMetrics;
private _tripScope;
private _resetScope;
private _transitionToHalfOpen;
private _emitStateChange;
private _shouldCountError;
private _isUrlExcluded;
private _createCircuitStateError;
private _getScopeMetrics;
private _getVisibleCounter;
private _getVisibleFailuresInWindow;
}
declare class CircuitBreakerStateError extends AxiosError {
readonly circuitState: CircuitBreakerState;
constructor(message: string, circuitState: CircuitBreakerState, request: AxiosRequestConfig, code?: string, response?: AxiosResponse);
}
/**
* Creates a CircuitBreakerPlugin instance.
* Functional alternative to using the `new CircuitBreakerPlugin()` constructor.
*
* The circuit breaker pattern prevents repeated requests to a failing service by
* temporarily blocking requests after a threshold of failures is reached.
*
* @param options Configuration options for the CircuitBreakerPlugin
* @returns A configured CircuitBreakerPlugin instance
*
* @example
* ```typescript
* const circuitBreaker = createCircuitBreaker({
* failureThreshold: 5, // Trip circuit after 5 consecutive failures
* openTimeout: 30000, // Remain open for 30s before allowing half-open test
* halfOpenMax: 1, // Allow 1 test request in half-open state
* useSlidingWindow: true, // Use sliding window for failure analysis
* slidingWindowSize: 60000 // 60-second sliding window
* });
*
* manager.use(circuitBreaker);
* ```
*/
declare function createCircuitBreaker(options?: Partial<CircuitBreakerOptions>): CircuitBreakerPlugin;
interface SanitizeOptions {
sensitiveHeaders?: readonly string[];
sensitiveFields?: readonly string[];
redactionChar?: string;
sanitizeRequestData?: boolean;
sanitizeResponseData?: boolean;
sanitizeUrlParams?: boolean;
allowedFields?: readonly string[];
allowlistOnly?: boolean;
}
interface DebugSanitizationPluginOptions {
sanitizeOptions?: SanitizeOptions;
}
/**
* Plugin that adds sanitized debug logging for requests and errors.
*
* Lazy-loads the sanitization module and logs sanitized request URLs, headers,
* data, and error responses via the manager's logger.
*
* @example
* ```typescript
* import { DebugSanitizationPlugin } from 'axios-retryer/plugins/DebugSanitizationPlugin';
*
* const debugPlugin = new DebugSanitizationPlugin({
* sanitizeOptions: { sensitiveHeaders: ['x-custom-secret'] }
* });
* manager.use(debugPlugin);
* ```
*/
declare class DebugSanitizationPlugin implements RetryPlugin {
name: string;
version: string;
private context;
private interceptorIdReq;
private interceptorIdRes;
private readonly sanitizeOptions;
constructor(options?: DebugSanitizationPluginOptions);
initialize(context: PluginContext): void;
onBeforeDestroyed(context: PluginContext): void;
private logSanitizedRequest;
private logSanitizedError;
}
/**
* Creates a DebugSanitizationPlugin instance.
* Functional alternative to using the `new DebugSanitizationPlugin()` constructor.
*
* The debug sanitization plugin adds sanitized debug logging for requests and errors,
* redacting sensitive information like tokens, passwords, and API keys.
*
* @param options Configuration options for the DebugSanitizationPlugin
* @returns A configured DebugSanitizationPlugin instance
*
* @example
* ```typescript
* const debugPlugin = createDebugSanitizationPlugin({
* sanitizeOptions: {
* sensitiveHeaders: ['x-custom-secret'],
* redactionChar: '#'
* }
* });
*
* manager.use(debugPlugin);
* ```
*/
declare function createDebugSanitizationPlugin(options?: DebugSanitizationPluginOptions): DebugSanitizationPlugin;
interface ManualRetryPluginEvents {
onManualRetryProcessStarted?: () => void;
onRequestRemovedFromStore?: (request: AxiosRequestConfig) => void;
}
interface ManualRetryPluginOptions {
maxRequestsToStore?: number;
manualRetryMaxAge?: number;
storeNonIdempotent?: boolean;
storeAuthRequests?: boolean;
beforeRetry?: (config: AxiosRequestConfig) => AxiosRequestConfig | null;
prepareRequestForStore?: (config: AxiosRequestConfig) => AxiosRequestConfig | null;
rehydrateAuth?: (config: AxiosRequestConfig) => AxiosRequestConfig | null;
requestStore?: RequestStore;
}
/**
* Plugin that stores failed requests and allows replaying them later via `retryFailedRequests()`.
*
* Listens to the `onFailure` event to capture terminal failures and strips auth headers
* before storage. Auth material is NOT re-applied on replay unless an explicit
* `rehydrateAuth` hook is provided, preventing cross-principal replay.
*
* @example
* ```typescript
* import { ManualRetryPlugin } from 'axios-retryer/plugins/ManualRetryPlugin';
*
* const manualRetry = new ManualRetryPlugin({ manualRetryMaxAge: 60_000 });
* manager.use(manualRetry);
*
* // Later, after failures:
* const results = await manualRetry.retryFailedRequests();
* ```
*/
declare class ManualRetryPlugin implements RetryPlugin<ManualRetryPluginEvents> {
name: string;
version: string;
readonly _events?: Readonly<ManualRetryPluginEvents>;
private context;
private store;
private readonly maxRequestsToStore;
private readonly maxAge;
private readonly storeNonIdempotent;
private readonly storeAuthRequests;
private readonly beforeRetryCallback?;
private readonly prepareRequestForStoreCallback?;
private readonly rehydrateAuthCallback?;
private readonly customStore?;
private onFailureHandler;
constructor(options?: ManualRetryPluginOptions);
initialize(context: PluginContext<ManualRetryPluginEvents>): void;
onBeforeDestroyed(context: PluginContext<ManualRetryPluginEvents>): void;
/**
* Retries all stored failed requests that have not expired.
* Requests older than `manualRetryMaxAge` are discarded.
*
* Replay is fail-fast: if any replayed request fails, the promise rejects
* with that error and remaining stored requests are not replayed.
*
* @returns Array of replay responses.
*/
retryFailedRequests<T = unknown>(): Promise<AxiosResponse<T>[]>;
/**
* Returns a copy of all currently stored failed requests.
*/
getStoredRequests(): AxiosRequestConfig[];
/**
* Clears all stored failed requests without retrying them.
*/
clearStoredRequests(): void;
private isEligible;
private prepareStoredRequest;
/**
* Prevents Axios from merging auth headers from `defaults.headers.common`
* into the replayed request. Without this, a request that failed under user A
* could be replayed with user B's token if the defaults changed between
* failure and replay.
*/
private neutralizeDefaultAuthHeaders;
}
/**
* Creates a ManualRetryPlugin instance.
* Functional alternative to using the `new ManualRetryPlugin()` constructor.
*
* The manual retry plugin stores failed requests and allows replaying them
* later via `retryFailedRequests()`.
*
* @param options Configuration options for the ManualRetryPlugin
* @returns A configured ManualRetryPlugin instance
*
* @example
* ```typescript
* const manualRetry = createManualRetryPlugin({
* manualRetryMaxAge: 60000, // Discard requests older than 1 minute
* maxRequestsToStore: 100, // Store at most 100 failed requests
* storeNonIdempotent: false, // Only store idempotent requests
* });
*
* manager.use(manualRetry);
*
* // Later:
* const results = await manualRetry.retryFailedRequests();
* ```
*/
declare function createManualRetryPlugin(options?: ManualRetryPluginOptions): ManualRetryPlugin;
interface MetricsPluginEvents {
onMetricsUpdated?: (metrics: AxiosRetryerDetailedMetrics) => void;
}
/**
* Plugin that enables detailed metrics collection for the RetryManager.
*
* Without this plugin, `getMetrics()` returns empty/zero metrics.
* Install this plugin to track request counts, retry distributions,
* error types, queue wait times, and priority breakdowns.
*
* @example
* ```typescript
* import { MetricsPlugin } from 'axios-retryer/plugins/MetricsPlugin';
*
* const metricsPlugin = new MetricsPlugin();
* manager.use(metricsPlugin);
*
* // Later: read collected metrics
* const metrics = manager.getMetrics();
* ```
*/
declare class MetricsPlugin implements RetryPlugin<MetricsPluginEvents> {
name: string;
version: string;
readonly _events?: Readonly<MetricsPluginEvents>;
private context;
private metrics;
private collector;
private readonly recorder;
private readonly onRequestQueuedListener;
private readonly onRequestDispatchedListener;
private readonly onRequestCancelledListener;
private readonly beforeRetryListener;
private readonly afterRetryListener;
private readonly onRetryScheduledListener;
private readonly onFailureListener;
private readonly onBlockingRequestFailedListener;
private readonly onRequestSucceededListener;
constructor();
initialize(context: PluginContext<MetricsPluginEvents>): void;
onBeforeDestroyed(context: PluginContext<MetricsPluginEvents>): void;
getMetrics(): AxiosRetryerDetailedMetrics;
resetMetrics(): void;
private emitMetricsUpdated;
}
declare class MetricsCollector implements MetricsRecorder {
private readonly getMetricsState;
private queueWaitHistory;
private retryDelayHistory;
constructor(getMetricsState: () => AxiosRetryerMetrics);
recordRequestStart(priority: AxiosRetryerRequestPriority): void;
recordQueueWait(durationMs: number): void;
recordRetrySuccess(priority: AxiosRetryerRequestPriority): void;
recordRetryFailure(priority: AxiosRetryerRequestPriority, error: AxiosError): void;
recordRetryAttempt(attempt: number, priority: AxiosRetryerRequestPriority): void;
recordRetryDelay(durationMs: number): void;
reset(): void;
recordCancellation(includeErrorType?: boolean): void;
recordTerminalFailure(isCritical: boolean): void;
buildDetailedMetrics(timerStats: {
activeTimers: number;
activeRetryTimers: number;
}): AxiosRetryerDetailedMetrics;
private getPriorityMetrics;
private recordDuration;
private pruneDurationHistory;
private getWindowAverage;
}
/**
* Creates a MetricsPlugin instance.
* Functional alternative to using the `new MetricsPlugin()` constructor.
*
* @returns A configured MetricsPlugin instance
*
* @example
* ```typescript
* import { createMetricsPlugin } from 'axios-retryer/plugins/MetricsPlugin';
*
* const metricsPlugin = createMetricsPlugin();
* manager.use(metricsPlugin);
* ```
*/
declare function createMetricsPlugin(): MetricsPlugin;
interface TokenRefreshResult {
/**
* New access token. When `null` or `undefined` (or omitted), the plugin treats the refresh as a no-op:
* no header update, no `onTokenRefreshed` / `onTokenRefreshFailed`, and no “failed refresh” short-circuit.
* Concurrent waiters are released without forcing `TokenRefreshFailedError`.
*/
token?: string | null;
}
type TokenRefreshHandler = (axiosInst: AxiosInstance) => Promise<TokenRefreshResult>;
interface TokenRefreshPluginEvents {
onTokenRefreshed?: (newToken: string) => void;
onTokenRefreshFailed?: () => void;
onBeforeTokenRefresh?: () => void;
}
interface TokenRefreshPluginOptions {
/** If true, allow multiple refresh attempts up to maxRefreshAttempts on failure. */
retryOnRefreshFail?: boolean;
/** Maximum number of refresh attempts (1 => 1 total attempt, 2 => 2 attempts, etc.). */
maxRefreshAttempts?: number;
/** Timeout in ms for each refresh call. */
refreshTimeout?: number;
/** The HTTP header name to set with the new token (e.g. "Authorization"). */
authHeaderName?: string;
/** A prefix for your token (commonly "Bearer "). */
tokenPrefix?: string;
/** HTTP status codes that trigger a token refresh (e.g., [401, 419]). */
refreshStatusCodes?: readonly number[];
/**
* Maximum backoff delay in ms between refresh retry attempts.
* Caps the exponential backoff to prevent multi-minute stalls with high maxRefreshAttempts.
* Default: 30_000 (30 seconds).
*/
maxRefreshBackoffMs?: number;
/**
* Maximum number of requests that may queue while a token refresh is in flight.
* Excess requests reject with TokenRefreshQueueOverflowError. Default: 500.
* Set to 0 or a negative number to disable the cap (not recommended).
*/
maxQueuedRequests?: number;
/**
* Optional function to detect auth errors in response bodies (for APIs that return 200 with error in body)
* Return true if response contains an auth error that should trigger token refresh
*/
customErrorDetector?: (response: unknown) => boolean;
}
/**
* A RetryPlugin that manages token refresh on certain status codes (e.g., 401).
* It intercepts failed requests, attempts to refresh the token,
* and re-dispatches any queued requests if refresh succeeds.
*
* Can also detect custom auth errors in response bodies for APIs that return 200 OK
* with error messages in the body (like GraphQL).
*/
declare class TokenRefreshPlugin implements RetryPlugin<TokenRefreshPluginEvents> {
name: string;
version: string;
readonly _events?: Readonly<TokenRefreshPluginEvents>;
private context;
private requestInterceptorId;
private interceptorId;
private responseInterceptorId;
private refreshAxios;
private isRefreshing;
private refreshQueue;
private readonly teardown;
private timerManager;
private failedAuthHeaderValue;
private readonly refreshToken;
private readonly options;
private logger;
constructor(refreshToken: TokenRefreshHandler, options?: TokenRefreshPluginOptions);
/**
* Called by RetryManager when we register this plugin via manager.use(plugin).
* Attaches a response interceptor to the manager's axios instance and
* creates a dedicated axios instance for refresh calls.
*/
initialize(context: PluginContext<TokenRefreshPluginEvents>): void;
/**
* Called when the plugin is removed.
*/
onBeforeDestroyed(context: PluginContext<TokenRefreshPluginEvents>): void;
private withTeardown;
private ensureActive;
private bindRefreshErrorToRequest;
/**
* Returns true when the queue is at or above `maxQueuedRequests`.
* A value <= 0 disables the cap.
*/
private isQueueOverflowing;
private buildQueueOverflowError;
private rejectQueueEntryWithBoundError;
private dispose;
/**
* Checks successful responses for custom auth errors in the response body
*/
private handleSuccessResponse;
/**
* Intercepts a failed response. If the error status is refreshable and the request
* hasn't already been retried, then either queues the request (if refresh is in progress)
* or starts a new refresh cycle.
*/
private handleResponseError;
/**
* Checks if the error status code is in the list of refreshable status codes.
*/
private isRefreshableError;
/**
* Main token refresh flow:
* 1) Set isRefreshing = true.
* 2) Attempt to refresh the token.
* 3) On success, update the auth header and retry both queued and original requests.
* 4) On failure, clear the queue and reject.
*/
private handleTokenRefresh;
/**
* Attempts token refresh up to maxRefreshAttempts times if retryOnRefreshFail is true.
* Each attempt is subject to a timeout defined in refreshTimeout. Throw a non-Axios
* error from the refresh handler to stop remaining refresh retries immediately, for
* example when no refresh token is available in storage.
*/
private executeTokenRefresh;
/**
* Updates the manager's default auth header so subsequent requests automatically carry the new token.
*/
private updateAuthHeader;
/**
* Retries the given request through the retry manager pipeline,
* marking it with __isRetryRefreshRequest to avoid loops.
*/
private retryRequest;
/**
* If a 401 is encountered while a refresh is already in progress, queue the