Docs Guides

Migration Guide: 1.x → 2.0

v2.0 makes the core-vs-plugin boundary explicit. Most changes are additive — the primary work is moving configuration from core options into the relevant plugins.

Upgrade checklist

  1. Upgrade axios to >= 1.7.4
  2. Move sanitization config into DebugSanitizationPlugin
  3. Install MetricsPlugin if you rely on live metric counters or onMetricsUpdated
  4. Prefer documented imports from axios-retryer/plugins/<Plugin> (or the barrel for convenience)
  5. Update TypeScript listeners for plugin events: widen the manager type via use() or createRetryer<PluginEvents>()
  6. Re-check maxRefreshAttempts if you use TokenRefreshPlugin
  7. Move manual replay to ManualRetryPlugin (root replay surface removed)
  8. Build the browser bundle locally if you need it (pnpm build:browser)
  9. Replace hooks: { ... } on createRetryer() with retryer.on(...) after construction
  10. Remove RequestDependencyPlugin; use blockingPriorityThreshold / cancelPendingOnDependencyFailure on the core manager

Sanitization → DebugSanitizationPlugin

Before (1.x)

const retryer = createRetryer({
  debug: true,
  enableSanitization: true,
  sanitizeOptions: {
    sensitiveHeaders: ['X-API-Key'],
  },
});

After (2.0)

import { createRetryer } from 'axios-retryer';
import { createDebugSanitizationPlugin } from 'axios-retryer/plugins/DebugSanitizationPlugin';

const retryer = createRetryer({ debug: true }).use(
  createDebugSanitizationPlugin({
    sanitizeOptions: { sensitiveHeaders: ['X-API-Key'] },
  }),
);

Metrics → MetricsPlugin

The core still exposes getMetrics(), but counters stay at zero without the plugin. onRetryProcessFinished remains a core lifecycle event but no longer carries a metrics payload — use onMetricsUpdated with MetricsPlugin when you need snapshots.

Before (1.x)

const retryer = createRetryer();

retryer.on('onMetricsUpdated', (metrics) => {
  console.log(metrics.successfulRetries);
});

After (2.0)

import { createRetryer } from 'axios-retryer';
import { createMetricsPlugin } from 'axios-retryer/plugins/MetricsPlugin';

const retryer = createRetryer().use(createMetricsPlugin());

retryer.on('onMetricsUpdated', (metrics) => {
  console.log(metrics.successfulRetries);
});

Manual replay → ManualRetryPlugin

Before (1.x)

const retryer = createRetryer({
  mode: RETRY_MODES.MANUAL,
  maxRequestsToStore: 100,
  hooks: {
    beforeManualRetry: (config) => config,
  },
});

const responses = await retryer.retryFailedRequests();

After (2.0)

import { createRetryer, RETRY_MODES } from 'axios-retryer';
import { createManualRetryPlugin } from 'axios-retryer/plugins/ManualRetryPlugin';

const manualRetry = createManualRetryPlugin({
  maxRequestsToStore: 100,
  beforeRetry: (config) => config,
});

const retryer = createRetryer({ mode: RETRY_MODES.MANUAL }).use(manualRetry);

const responses = await manualRetry.retryFailedRequests();

maxRefreshAttempts off-by-one fix

In 1.x, maxRefreshAttempts: 3 produced 4 total attempts due to a loop bug. In 2.0 it means exactly 3.

// If you relied on the old behaviour (4 attempts):
createTokenRefreshPlugin(refreshFn, { maxRefreshAttempts: 4 });  // Now truly 4 attempts

// If your value was already tuned:
createTokenRefreshPlugin(refreshFn, { maxRefreshAttempts: 3 });  // Now truly 3 attempts (was effectively 4)
// → decrease by 1 if you want to keep the same total attempt count

Constructor hooks removed

In 2.0, RetryManagerOptions.hooks is gone. Register core and plugin listeners with retryer.on(...) on the instance (after use() when the event is plugin-specific).

Plugin event typing

Plugin-specific events are typed on the manager returned from use(). You can pass a generic to createRetryer<Events>() for early core listeners, but plugin events are still registered with .on() after attaching the plugin.

Before (1.x)

createRetryer({
  hooks: {
    onTokenRefreshed: (token) => console.log(token),
  },
});

After (2.0) — use() then on()

import { createRetryer } from 'axios-retryer';
import { createTokenRefreshPlugin } from 'axios-retryer/plugins/TokenRefreshPlugin';

const withRefresh = createRetryer().use(
  createTokenRefreshPlugin(async (axiosInstance) => {
    const { data } = await axiosInstance.post('/auth/refresh');
    return { token: data.accessToken };
  }),
);
withRefresh.on('onTokenRefreshed', (token) => console.log(token));

After (2.0) — listener after use() only

import { createRetryer } from 'axios-retryer';
import { createTokenRefreshPlugin } from 'axios-retryer/plugins/TokenRefreshPlugin';

// Or assign the widened manager from an existing instance:
const retryer = createRetryer();
const withRefresh = retryer.use(
  createTokenRefreshPlugin(async (axiosInstance) => {
    const { data } = await axiosInstance.post('/auth/refresh');
    return { token: data.accessToken };
  }),
);
withRefresh.on('onTokenRefreshed', (token) => console.log(token));

Import guidance

Per-plugin subpaths are preferred for tree-shaking; the barrel is still supported.

import { createTokenRefreshPlugin } from 'axios-retryer/plugins/TokenRefreshPlugin';
import { createMetricsPlugin } from 'axios-retryer/plugins/MetricsPlugin';
import { createTokenRefreshPlugin, createMetricsPlugin } from 'axios-retryer/plugins';

Avoid deep imports from src/ or undocumented paths.

Legacy plugin.hooks removed

Plugins can no longer declare hooks via a plugin.hooks property. Use retryer.on() inside initialize():

// Inside a custom plugin's initialize():
plugin.initialize = (retryer) => {
  retryer.on('beforeRetry', (config) => {
    // Custom logic
  });
};

Caching invalidation

CachingPlugin.invalidateCache() uses explicit semantics: a bare string matches the exact cache key (not a substring). Prefer exact / prefix object matchers or a RegExp (see example below).

const cachePlugin = createCachePlugin();
const userKey = cachePlugin.buildCacheKey({ method: 'get', url: '/users/1' });

cachePlugin.invalidateCache({ exact: userKey });
cachePlugin.invalidateCache({ prefix: 'GET|/users/' });

Typical end state

Most 1.x apps end up close to this shape:

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,
})
  .use(createMetricsPlugin())
  .use(
    createDebugSanitizationPlugin({
      sanitizeOptions: {
        sensitiveHeaders: ['Authorization'],
      },
    }),
  )
  .use(
    createTokenRefreshPlugin(async (axiosInstance) => {
      const { data } = await axiosInstance.post('/auth/refresh');
      return { token: data.accessToken };
    }),
  );

2.1.6 behavior notes

Applies when upgrading to 2.1.6 from an earlier 2.0.x or 2.1.x release (for example 2.0.52.1.6). Extends the checklist above — not a second migration from 1.x.

  • Queue teardown error class: On RetryManager destroy, waiters still in the queue get QueueDestroyedError. Earlier 2.0.x / 2.1.x could surface QueueClearedError on that path — handle QueueDestroyedError or both if you branch on instanceof.
  • Circuit breaker default scope without host: Default host+url no longer collapses relative URLs into one __global__ bucket; keys use the normalized path (or unknown). Use a custom scope callback if you need the old aggregation.
  • TokenRefreshPlugin customErrorDetector: If it throws on a success response, the error is logged and the response is returned (no body-triggered refresh).
  • Internal errors: Automatic retries are skipped for internal queue/cancel outcomes (QUEUE_DESTROYED, QUEUE_CLEARED, QUEUE_FULL, REQUEST_CANCELED, EREQUEST_ABORTED).
  • Retry-After: Read in a header-tolerant, case-insensitive way — correctness fix, no API change.
  • Manual replay: Failed manual replays are not re-stored (avoids recursive store loops).

Browser bundle

The UMD browser bundle is no longer published as a built artifact. Generate it locally:

pnpm build:browser
# Output: dist/browser/

Notes

  • 1.5.4 was prepared but not published; its planned fixes ship in 2.0.0.
  • Dependency-style gating (block lower-priority work until blocking requests finish) is core: blockingPriorityThreshold and cancelPendingOnDependencyFailure. See the Concurrency guide.
  • The axios-retryer/plugins barrel is deprecated for tree-shaking; it may be removed in v3.

axios-retryer/plugins barrel — v3 roadmap

The barrel pulls every plugin into one chunk (~52 KB raw / ~14 KB gzip). Subpath imports let bundlers drop unused plugins.

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';
Back to Docs → MIGRATION.md ↗ Changelog ↗