axios-retryer 2.0 makes the core-vs-plugin boundary explicit.
The main idea is simple:
- keep the root entry focused on retry management, queueing, events, and shared types
- move optional behavior like sanitization and populated metrics behind opt-in plugins
- keep plugin imports documented and tree-shakeable
If you are upgrading from any 1.x release, use this checklist.
- Upgrade
axiosto>= 1.7.4. - Move sanitization config out of
createRetryer()and intoDebugSanitizationPlugin. - Install
MetricsPluginif you rely on populatedgetMetrics()data oronMetricsUpdated. - Prefer documented plugin imports from
axios-retryer/pluginsor per-plugin entry points. - Update any TypeScript code that listens to plugin-specific events so the manager type is widened through
use()or an explicit generic. - Re-check
maxRefreshAttemptsif you useTokenRefreshPlugin. - Move any manual replay usage to
ManualRetryPlugin. RootretryFailedRequests(),maxRequestsToStore,requestStore, andbeforeManualRetryare removed in2.0. - If you used the browser bundle, build it locally with
pnpm build:browser. - Replace
hooks: { ... }oncreateRetryer()withretryer.on(...)after construction (and afteruse()when you need plugin-typed events). - If you imported
RequestDependencyPlugin, remove it and useblockingPriorityThreshold/cancelPendingOnDependencyFailureoncreateRetryer()instead.
In 1.x, you could pass hooks into createRetryer({ hooks: { ... } }). In 2.0, that option is removed — use the event API on the manager instance.
Before:
import { createRetryer } from 'axios-retryer';
const retryer = createRetryer({
hooks: {
onFailure: (config) => console.log(config.url),
},
});After:
import { createRetryer } from 'axios-retryer';
const retryer = createRetryer();
retryer.on('onFailure', (config) => {
console.log(config.url);
});The published package no longer includes RequestDependencyPlugin or the ./plugins/RequestDependencyPlugin export. Use core options blockingPriorityThreshold and cancelPendingOnDependencyFailure, and listen to onBlockingRequestFailed / onAllBlockingRequestsResolved / onRequestCancelled as needed. See the concurrency guide and configuration docs.
If you registered a custom metrics recorder (uncommon), update it to the new MetricsRecorder shape: reset, buildDetailedMetrics, and optional emitMetricsUpdated only.
In 1.x, sanitization lived on the core manager options. In 2.0, it is an explicit plugin.
Before:
import { createRetryer } from 'axios-retryer';
const retryer = createRetryer({
debug: true,
enableSanitization: true,
sanitizeOptions: {
sensitiveHeaders: ['X-API-Key'],
},
});After:
import { createRetryer } from 'axios-retryer';
import { createDebugSanitizationPlugin } from 'axios-retryer/plugins';
const retryer = createRetryer({ debug: true });
retryer.use(
createDebugSanitizationPlugin({
sanitizeOptions: {
sensitiveHeaders: ['X-API-Key'],
},
}),
);The core manager still exposes getMetrics(), but the live counters now stay at their zero defaults unless you install MetricsPlugin.
Before:
const retryer = createRetryer();
retryer.on('onMetricsUpdated', (metrics) => {
console.log(metrics.successfulRetries);
});onRetryProcessFinished remains a core lifecycle event, but it no longer carries a metrics payload. Use onMetricsUpdated when you need snapshots.
After:
import { createRetryer } from 'axios-retryer';
import { createMetricsPlugin } from 'axios-retryer/plugins';
const retryer = createRetryer().use(createMetricsPlugin());
retryer.on('onMetricsUpdated', (metrics) => {
console.log(metrics.successfulRetries);
});In 1.x, manual replay could be driven from the root manager. In 2.0, that replay surface lives entirely on ManualRetryPlugin.
Before:
import { createRetryer, RETRY_MODES } from 'axios-retryer';
const retryer = createRetryer({
mode: RETRY_MODES.MANUAL,
maxRequestsToStore: 100,
hooks: {
beforeManualRetry: (config) => config,
},
});
const responses = await retryer.retryFailedRequests();After:
import { createRetryer, RETRY_MODES } from 'axios-retryer';
import { createManualRetryPlugin } from 'axios-retryer/plugins/ManualRetryPlugin';
const retryer = createRetryer({ mode: RETRY_MODES.MANUAL });
const manualRetry = createManualRetryPlugin({
maxRequestsToStore: 100,
beforeRetry: (config) => config,
});
retryer.use(manualRetry);
const responses = await manualRetry.retryFailedRequests();In 1.x, plugin-specific hook names were present on the shared hook type. In 2.0, there is no hooks option — use retryer.on(...) after use() (or pass a generic to createRetryer for early core listeners only).
Before:
import { createRetryer } from 'axios-retryer';
createRetryer({
hooks: {
onTokenRefreshed: (token) => {
console.log(token);
},
},
});After:
import { createRetryer } from 'axios-retryer';
import { createTokenRefreshPlugin, type TokenRefreshPluginEvents } from 'axios-retryer/plugins/TokenRefreshPlugin';
const retryer = createRetryer();
const withRefresh = retryer.use(createTokenRefreshPlugin(async () => ({ token: 'fresh-token' })));
withRefresh.on('onTokenRefreshed', (token) => {
console.log(token);
});Optional: if you want the generic on the root manager before plugins attach, you can write createRetryer<TokenRefreshPluginEvents>() and still register plugin events only after use().
1.x effectively attempted refresh maxRefreshAttempts + 1 times because of an off-by-one bug.
Before:
createTokenRefreshPlugin(refreshFn, {
maxRefreshAttempts: 3,
});After:
createTokenRefreshPlugin(refreshFn, {
maxRefreshAttempts: 3,
});The code looks the same, but the behavior is different:
1.x:3meant4total attempts2.0:3means3total attempts
If you tuned this number around the old bug, lower it by 1 to preserve the previous total-attempt behavior.
The browser bundle is no longer treated as a default published artifact in package metadata. Build it locally when you need a script-tag bundle:
pnpm build:browserThese import styles are supported in 2.0, but the per-plugin subpaths are the preferred documented path:
import { createRetryer } from 'axios-retryer';
import { createTokenRefreshPlugin } from 'axios-retryer/plugins/TokenRefreshPlugin';
import { createMetricsPlugin } from 'axios-retryer/plugins/MetricsPlugin';import { createTokenRefreshPlugin, createMetricsPlugin } from 'axios-retryer/plugins';Use the barrel when convenience matters more than explicitness, and avoid deep internal imports from src/ or undocumented paths.
Plugins must subscribe through retryer.on(...) inside initialize(). The deprecated plugin.hooks field no longer runs in 2.0.
plugin.initialize = (retryer) => {
retryer.on('beforeRetry', (config) => {
console.log(config.url);
});
};CachingPlugin.invalidateCache() now prefers explicit exact-key or prefix matching instead of substring semantics.
Preferred direction:
const cachePlugin = createCachePlugin();
const userKey = cachePlugin.buildCacheKey({ method: 'get', url: '/users/1' });
cachePlugin.invalidateCache({ exact: userKey });
cachePlugin.invalidateCache({ prefix: 'GET|/users/' });Most 1.x applications end up looking like this after migration:
import { createRetryer, RETRY_MODES } from 'axios-retryer';
import { createDebugSanitizationPlugin, createMetricsPlugin, createTokenRefreshPlugin } from 'axios-retryer/plugins';
const retryer = createRetryer({
mode: RETRY_MODES.AUTOMATIC,
retries: 3,
debug: true,
});
retryer.use(createMetricsPlugin());
retryer.use(
createDebugSanitizationPlugin({
sanitizeOptions: {
sensitiveHeaders: ['Authorization'],
},
}),
);
retryer.use(
createTokenRefreshPlugin(async (axiosInstance) => {
const { data } = await axiosInstance.post('/auth/refresh');
return { token: data.accessToken };
}),
);These notes apply when upgrading to 2.1.6 from an earlier 2.0.x or 2.1.x release (for example 2.0.5 → 2.1.6). They extend the 1.x → 2.0 checklist above — not a second migration from 1.x.
When RetryManager is destroyed while requests are still waiting in the queue, those waiters are rejected with QueueDestroyedError. Earlier 2.0.x / 2.1.x builds could surface QueueClearedError on that path. If you use instanceof in catch, handle QueueDestroyedError (or treat both teardown errors the same).
With default host+url scope, relative requests (no host) no longer share a single internal __global__ bucket. Scoping uses the normalized path (or unknown) per pattern instead, which can change when the circuit trips. Use an explicit scope callback if you need the old “everything relative in one bucket” behavior.
If customErrorDetector throws while inspecting a successful response body, the error is logged and the response is returned (no refresh triggered from that path). Do not rely on throwing to abort handling.
Automatic retries are not scheduled for internal queue/cancel outcomes (for example QUEUE_DESTROYED, QUEUE_CLEARED, QUEUE_FULL, REQUEST_CANCELED, EREQUEST_ABORTED).
Retry-After is read in a header-shape-tolerant, case-insensitive way (including Axios header objects). This is a correctness fix; the public API is unchanged.
ManualRetryPlugin does not re-store a request that already failed during a manual replay, avoiding recursive store loops.
1.5.4was prepared but not published. The fixes and API cleanup tracked for that release are included in2.0.0.- Dependency-style gating (hold lower-priority work until blocking requests finish) is implemented in the core via
blockingPriorityThresholdandcancelPendingOnDependencyFailure. There is no separateRequestDependencyPluginin the published package; use the concurrency guide and configuration docs. - The dedicated plugin barrel
axios-retryer/pluginsis kept for compatibility but is deprecated. Prefer focused subpath imports (e.g.axios-retryer/plugins/CachingPlugin) for better tree-shaking.
Measured bundle impact: The axios-retryer/plugins barrel bundles all plugins unconditionally (~52.6 KB raw / ~13.8 KB gzip). Importing from subpaths (e.g. axios-retryer/plugins/CachingPlugin) allows bundlers to tree-shake unused plugins, reducing per-plugin cost to ~5–15 KB raw.
Decision: The barrel will be removed in the next major release (v3.0.0). Users should migrate to subpath imports now.
Migration: Replace:
import { CachingPlugin, TokenRefreshPlugin } from 'axios-retryer/plugins';With focused imports:
import { CachingPlugin } from 'axios-retryer/plugins/CachingPlugin';
import { TokenRefreshPlugin } from 'axios-retryer/plugins/TokenRefreshPlugin';