# axios-auth-refresh: retrying a 401 without the user seeing a flicker

> axios-auth-refresh is a small axios plugin that intercepts failed requests, runs your refresh logic, and replays the original request with the new credential. Its real job is concurrency, since it stalls requests that arrive while a refresh is in flight and resolves them together when the token is ready.

**Flyrell/axios-auth-refresh** — Library that helps you implement automatic refresh of authorization via axios interceptors. You can easily intercept the original request when it fails, refresh the authorization and continue with the original request, without user even noticing.

- Repository: https://github.com/Flyrell/axios-auth-refresh
- Stars: 1,142 · Forks: 94
- Language: TypeScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/flyrell-axios-auth-refresh

## The problem is the race, not the 401

The package description is precise about what it does. It helps you implement automatic refresh of authorization via axios interceptors, and you can intercept the original request when it fails, refresh the authorization and continue with the original request, without any user interaction.

The naive version of this feature is about ten lines of code: register a response interceptor, check for a 401, call the refresh endpoint, set the new header, retry. The problem is not the retry. The problem is what happens when five components fire five requests at once and the token has expired.

All five get a 401. All five see the interceptor. If each one independently calls the refresh endpoint, you have five refresh calls, and depending on the backend the first one may invalidate the token the other four just received. The classic outcome is a cascade of failed retries and a user staring at an error state that resolves on a manual refresh.

That is the problem this library exists to solve, and the README states it directly: the plugin stalls additional requests that have come in while waiting for a new authorization token and resolves them when a new token is available. The requests made during the refresh window are bound onto the promise returned by the refresh function, so they resolve when a new access token has been fetched, or when the refreshing logic failed.

The audience is a frontend team with a token-based API where the access token expires in minutes and the refresh token does not. It is MIT licensed, written in TypeScript, and the topic list mentions stalled-requests explicitly, which tells you the queueing behaviour is the feature the author considers central.

## createAuthRefresh takes your axios instance and your function

The API is a single function with three parameters.

```typescript
createAuthRefresh(
    axios: AxiosInstance,
    refreshAuthLogic: (failedRequest: any) => Promise<any>,
    options: AxiosAuthRefreshOptions = {}
): number;
```

The first argument is the axios instance you want the interceptors bound to. The second is your refresh function, which the README is emphatic about: it must return a promise, and it accepts exactly one parameter, the failedRequest returned by the original call. The third is an optional options object. The return value is the interceptor id, which the README notes is there in case you want to reject it manually.

What the library does not do is tell you what a refresh looks like. The README is explicit that what happens when the request fails due to authorization is all up to you, and that you can either run a refresh call for a new authorization token or run custom logic.

The usage example shows the shape of a refresh function. It posts to a token endpoint, stores the token, mutates the failed request's config headers, and resolves.

```javascript
const refreshAuthLogic = (failedRequest) =>
    axios.post('https://www.example.com/auth/token/refresh').then((tokenRefreshResponse) => {
        localStorage.setItem('token', tokenRefreshResponse.data.token);
        failedRequest.response.config.headers['Authorization'] = 'Bearer ' + tokenRefreshResponse.data.token;
        return Promise.resolve();
    });

createAuthRefresh(axios, refreshAuthLogic);
```

Note the mutation of failedRequest.response.config.headers. That is how the retry picks up the new credential: you change the original request's config in place, and the library replays that request. It also means your refresh function has to reach the same place the token will be read from later, which is why the README separately recommends wrapping your token read in a function.

## Deduplication, and the loop you create if you turn it off

The deduplicateRefresh option is the newest and most consequential of the options, and it is on by default.

By default, deduplicateRefresh is true, and the interceptor ensures that only one refresh call runs at a time. While the refresh is in progress the axios instance is paused, any new requests are queued, and they are resolved automatically once the refresh completes. The README calls this the recommended behavior for most applications.

The alternative is for cases where every failed request should independently trigger its own refresh cycle, and you set deduplicateRefresh to false.

```javascript
{
    deduplicateRefresh: false, // default: true
}
```

The consequence of turning it off is stated in the next line of the README: you must mark your refresh calls with skipAuthRefresh to avoid interceptor loops. That is the failure mode to understand. If the refresh call itself returns a 401, and it is not marked to skip the interceptor, the interceptor fires again, calls refresh again, and the loop continues until something times out or the stack runs out.

The same skipAuthRefresh option exists for ordinary calls. You pass it in the request config for any request you do not want intercepted.

```javascript
axios.get('https://www.example.com/', { skipAuthRefresh: true });
```

In TypeScript you can import the custom request config interface from the package, AxiosAuthRefreshRequestConfig, so the option is typed rather than a magic string. That is a small quality signal about the library's TypeScript support, and it matters because the option is otherwise easy to typo and hard to discover.

## Status codes, custom predicates and the CORS problem

By default the interceptor runs on a 401. The statusCodes option lets you intercept several codes, and the example shows adding 403.

```javascript
{
    statusCodes: [401, 403], // default: [ 401 ]
}
```

The shouldRefresh option is the more precise mechanism. You provide a function that decides whether a failed request should trigger a refresh, and the README notes that when shouldRefresh is configured, the statusCodes logic is ignored. The example checks a business error code in the response body rather than the HTTP status.

```javascript
{
    shouldRefresh: (error) =>
        error?.response?.data?.business_error_code === 100385,
}
```

That is the option to reach for when your API signals expiry with a 200 and an error payload, which is common in mobile-first backends.

The last option, interceptNetworkError, exists for a specific browser failure mode and comes with a warning. Some CORS APIs may not return CORS response headers when an HTTP 401 response is returned, so the browser cannot read the response headers to determine the status code, and axios sees a network error rather than a 401. Enabling interceptNetworkError catches any network error, and the README marks it as a last resort, because if you use it to work around an API that does not support CORS with a 401 response, your retry logic can end up testing for network connectivity by attempting to refresh authentication.

The CORS limitation is not a library defect. It is a browser rule, and a proxy is the real fix. But the option tells you the library author has run into it, which is useful context.

## Where the library ends and your code begins

Two options make clear that the library handles the mechanics and leaves the decisions to you.

The retryInstance option lets you specify which axios instance is used for retrying the stalled requests. The default is undefined, in which case the instance passed to createAuthRefresh is used. This matters when your refresh endpoint lives on a different base URL or needs different defaults than the API instance, and it avoids the trap of a retry that goes to the wrong host.

The onRetry callback is called before each stalled request is sent, with the request configuration object, and the example shows it modifying the config.

```javascript
{
    onRetry: (requestConfig) => ({ ...requestConfig, baseURL: '' }), // default: undefined
}
```

Beyond that, the README makes one recommendation that is a design constraint rather than a feature. Since the plugin automatically stalls additional requests while refreshing the token, it is a good idea to wrap your request logic in a function, to make sure the stalled requests are using the newly fetched data.

```javascript
function getAccessToken() {
    return localStorage.getItem('token');
}

axios.interceptors.request.use((request) => {
    request.headers['Authorization'] = `Bearer ${getAccessToken()}`;
    return request;
});
```

The reasoning is about ordering. A request that was created before the refresh finished must read the token at send time, not at creation time, or it will go out with the stale value and fail again. The request interceptor is the right place for that, and reading the token inside the function rather than capturing it in a closure is what makes the timing correct.

That is a small amount of discipline the library cannot enforce, and it is the most likely source of a stubborn intermittent failure if you skip it.

## A peer dependency on axios, a v5 release, and an MIT licence

axios-auth-refresh declares axios as a peer dependency with the range >= 1.0.0, and axios itself appears only in devDependencies at 1.13.6. That is the correct arrangement for an axios plugin, since the plugin binds interceptors onto an instance the host application owns, and the host must control the axios version.

The package is published as dual CommonJS and ESM, with main pointing at dist/index.cjs.js, module at dist/index.esm.js and types at dist/index.d.ts, and an exports map that serves the same three from the root. The files array ships only dist and LICENSE, so the source is not in the tarball. The build is rollup, the tests are jest with ts-jest, and the type check is a plain tsc --noEmit, which is a reasonable way to confirm the declarations and the implementation agree.

The examples directory is unusually well populated for a package of this size. There are files for a basic refresh, custom status codes, network error handling, the onRetry callback, a paused instance, skipping the auth refresh and an all-in-one runner, plus a helpers directory. There is an npm script to run them, tsx examples/run-all.ts, which means the documented behaviours are executable rather than only described.

The release history shows v5.0.2 from 2026-03-13, v5.0.1 from 2026-03-04 and v5.0.0 from 2026-03-03, so the major version landed in March 2026 with patch releases following within days. The last push was on 2026-05-08. That is within the last six months, so the tree is being worked on, but there has been no tagged release since March.

The licence is MIT, with the LICENSE file in the repository root, and the author is Dawid Zbiński. A Buy Me a Coffee link is at the top of the README, and the repository has a CHANGELOG.md, CONTRIBUTING.md, a.husky/ directory wired up by the prepare script, and a prettier configuration with pretty-quick for staged files.

## Conclusion

axios-auth-refresh suits a frontend team using axios with short-lived access tokens and a refresh endpoint, because the retry and the queueing of concurrent requests are handled for you and the refresh logic stays your own function rather than something the library dictates. It is the wrong tool for a project that does not use axios, since it is a peer dependency on axios 1.0.0 or newer, and it is the wrong choice if your backend returns 403 rather than 401 for an expired token, since you would need to add 403 to statusCodes or write a shouldRefresh predicate. Before you adopt it, confirm the deduplicateRefresh default is what you want, since disabling it requires you to mark the refresh call itself with skipAuthRefresh to avoid an interceptor loop.

## FAQ

### How do I install axios-auth-refresh?

Run npm install axios-auth-refresh --save, or yarn add axios-auth-refresh. The package declares axios as a peer dependency with the range >= 1.0.0, so your application must provide axios itself.

### How does axios-auth-refresh handle requests that arrive while a token refresh is running?

The plugin stalls those requests and binds them onto the promise returned by your refresh function, so they are resolved once a new access token has been fetched, or when the refresh logic failed.

### What happens if my API returns 403 instead of 401 for an expired token?

By default only 401 triggers the interceptor. You can set statusCodes to [401, 403], or provide a shouldRefresh function, in which case the statusCodes logic is ignored and your predicate decides.

### What does deduplicateRefresh do and what breaks if I disable it?

With deduplicateRefresh set to true, which is the default, only one refresh call runs at a time and the axios instance is paused while it is in progress. If you set it to false, the README says you must mark your refresh calls with skipAuthRefresh to avoid interceptor loops.

### How do I skip the interceptor for a specific request?

Pass the skipAuthRefresh option in the request config, for example axios.get('https://www.example.com/', { skipAuthRefresh: true }). In TypeScript you can import the AxiosAuthRefreshRequestConfig interface from the package.

## Sources

- [Flyrell/axios-auth-refresh on GitHub](https://github.com/Flyrell/axios-auth-refresh)
- [Issues](https://github.com/Flyrell/axios-auth-refresh/issues)
- [License: MIT](https://github.com/Flyrell/axios-auth-refresh/blob/master/LICENSE)
- [README](https://github.com/Flyrell/axios-auth-refresh/blob/master/README.md)
- [Releases](https://github.com/Flyrell/axios-auth-refresh/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/flyrell-axios-auth-refresh
