npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

axios-auth-refresh-queue

v2.0.2

Published

Bulletproof Axios Interceptor for JWT Refresh Token

Readme

Axios Auth Refresh Queue 🛡️

🚀 Ultra-lightweight (< 1KB) zero-dependency authentication interceptor for Axios.

A bulletproof, zero-config Axios interceptor that handles JWT refresh tokens automatically. It solves the race condition problem when multiple requests fail with 401 Unauthorized simultaneously.

License TypeScript npm version npm downloads install size JSR

License

MIT

🚀 Why use this?

When your Access Token expires, your app might fire 5 API requests at once. Without this library, all 5 will fail, leading to 5 separate "Refresh Token" calls (Race Condition) or forcing the user to logout.

Why this library instead of axios-auth-refresh? This library fixes it by:

  1. Intercepting the first 401 error.
  2. Pausing all other requests in a Queue.
  3. Calling the "Refresh Token" API once.
  4. Retrying all paused requests with the new token.

🌍 Compatibility & Supported Frameworks

Since this library is built on top of Axios, it is framework-agnostic and works in any JavaScript/TypeScript environment:

| Platform | Frameworks | Status | | :----------- | :-------------------------------------------- | :----------------- | | Frontend | React, Vue, Angular, Svelte, Next.js, Nuxt.js | ✅ Fully Supported | | Backend | Node.js (Express, NestJS), Deno, Bun | ✅ Fully Supported | | Mobile | React Native, Expo, Ionic, Capacitor | ✅ Fully Supported | | Desktop | Electron, Tauri | ✅ Fully Supported |

📦 Installation

npm install axios-auth-refresh-queue
# or
yarn add axios-auth-refresh-queue

🛠️ Usage

  1. Basic Setup Just wrap your axios instance with applyAuthTokenInterceptor.
import axios from "axios";
import { applyAuthTokenInterceptor } from "axios-auth-refresh-queue";

// 1. Create your axios instance
const apiClient = axios.create({
  baseURL: "https://api.your-backend.com", // 👈 REPLACE THIS with your actual API URL
});

// 2. Setup the interceptor
applyAuthTokenInterceptor(apiClient, {
  headerTokenHandler: (request) => {
    const token = localStorage.getItem("accessToken");
    if (token) {
      request.headers.Authorization = `Bearer ${token}`;
    }
  },
  // Method to get the refresh token from your storage
  // (You can use localStorage, sessionStorage, or cookies here)
  getRefreshToken: () => localStorage.getItem("refresh_token"),

  // Method to call your backend to refresh the token
  requestRefresh: async (refreshToken) => {
    // ⚠️ IMPORTANT: Implement your own refresh token API logic here
    const response = await axios.post(
      "https://api.your-backend.com/auth/refresh",
      {
        token: refreshToken,
      }
    );

    // Function must return this specific object structure
    return {
      accessToken: response.data.accessToken,
      refreshToken: response.data.refreshToken,
    };
  },

  // Callback when refresh succeeds
  onSuccess: (newTokens) => {
    // Logic to save new tokens to storage
    localStorage.setItem("access_token", newTokens.accessToken);
    if (newTokens.refreshToken) {
      localStorage.setItem("refresh_token", newTokens.refreshToken);
    }

    // Optional: Set default header for future requests
    apiClient.defaults.headers.common[
      "Authorization"
    ] = `Bearer ${newTokens.accessToken}`;
  },

  // Callback when refresh fails (e.g., Refresh token also expired)
  onFailure: (error) => {
    console.error("Session expired, logging out...");
    localStorage.clear();
    window.location.href = "/login"; // Redirect to login page
  },
});

export default apiClient;
  1. Custom Headers (Advanced) If your backend doesn't use Authorization: Bearer or requires specific headers (like x-api-key), you can use attachTokenToRequest.
applyAuthTokenInterceptor(apiClient, {
  // ... other configs

  attachTokenToRequest: (request, token) => {
    // Custom header logic
    request.headers["x-auth-token"] = token;
    request.headers["x-client-id"] = "my-app-v1";
  },
});

3. 🔥 Advanced: Secure Mode (HttpOnly Cookie & Memory)

This is the recommended setup for high-security applications to prevent XSS attacks.

  • Refresh Token: Stored in an HttpOnly Cookie (handled automatically by the browser).
  • Access Token: Stored in app memory (variables/state) only.
import axios from "axios";
import { applyAuthTokenInterceptor } from "axios-auth-refresh-queue";

// 1. Create instance with credentials for cookie support
const apiClient = axios.create({
  baseURL: "https://api.your-backend.com",
  withCredentials: true,
});

// Your in-memory store (could be a simple variable, Redux, or Zustand)
let accessTokenMemory: string | null = null;

applyAuthTokenInterceptor(apiClient, {
  // 🟢 Dynamically attach the token from memory before EVERY request
  headerTokenHandler: (config) => {
    if (accessTokenMemory) {
      config.headers.set("Authorization", `Bearer ${accessTokenMemory}`);
    }
  },

  // 🟢 Refresh Logic: Browser sends the HttpOnly cookie automatically
  requestRefresh: async () => {
    const response = await axios.post(
      "https://api.your-backend.com/auth/refresh",
      {},
      { withCredentials: true }
    );

    return {
      accessToken: response.data.accessToken,
      // No need to return refreshToken if it's managed via Set-Cookie header
    };
  },

  // 🟢 Update Memory when refresh succeeds
  onSuccess: (newTokens) => {
    accessTokenMemory = newTokens.accessToken;

    // Optional: If using Redux/Zustand
    // store.dispatch(setToken(newTokens.accessToken));
  },

  onFailure: (error) => {
    console.error("Session expired");
    accessTokenMemory = null;
    window.location.href = "/login";
  },
});

export default apiClient;

4. 🔒 Cross-Tab Synchronization (New in v2.0)

In a multi-tab environment, you don't want Tab A and Tab B to both refresh the token at the same time. This causes Race Conditions, especially if your backend uses Refresh Token Rotation (where a refresh token can only be used once).

How it works:

  1. Tab A detects 401, acquires a Browser Lock, and starts refreshing.

  2. Tab B detects 401, sees the lock is taken, and waits.

  3. Tab A finishes, saves the new token to LocalStorage, and releases the lock.

  4. Tab B wakes up, checks if the token in LocalStorage is valid (using checkTokenIsValid), and reuses it immediately without calling the API.

Setup:

You simply need to provide the checkTokenIsValid callback.

import { jwtDecode } from "jwt-decode"; // Optional: helper library

applyAuthTokenInterceptor(apiClient, {
  // ... other configs ...

  // ✅ REQUIRED for Cross-Tab Sync
  // This function runs when a tab wakes up from waiting.
  // Return the valid token string if found, otherwise return null/false.
  checkTokenIsValid: async () => {
    const token = localStorage.getItem("accessToken");
    if (!token) return null;

    // Example: Check if token is not expired
    const decoded = jwtDecode(token);
    if (decoded.exp * 1000 > Date.now()) {
      return token; // Token is fresh! Use it immediately.
    }

    return null; // Token is old, proceed to refresh.
  },
});

⏳ Configuration & Timeouts

By default, the interceptor waits 30 seconds for the refresh token API to respond. If the backend hangs or the network is too slow, the request will fail with a timeout error to prevent the app from being stuck indefinitely.

You can customize this duration using refreshTimeout:

applyAuthTokenInterceptor(apiClient, {
  // ... other options ...

  // ⚡ Fail fast: Abort if refresh takes more than 10 seconds
  refreshTimeout: 10000,

  onFailure: (error) => {
    // Error message will be: "Refresh token timed out after 10000ms"
    console.error(error.message);
    window.location.href = "/login";
  },
});

⏩ Skipping Auth Refresh

Sometimes you want to ignore specific requests (e.g., Login API, Health Checks) even if they return 401. You can pass skipAuthRefresh: true in the request config.

// This request will fail immediately on 401 without triggering refresh flow
axios.get("/api/public-data", {
  skipAuthRefresh: true,
});

// Useful for the Login endpoint itself to prevent infinite loops
axios.post("/api/login", data, {
  skipAuthRefresh: true,
});

🔧 Custom Status Codes

Some backends return 403 Forbidden instead of 401 Unauthorized when the token expires. You can customize which status codes trigger the refresh logic:

applyAuthTokenInterceptor(apiClient, {
  // ... other options

  // Trigger refresh on both 401 and 403
  statusCodes: [401, 403],
});

🐞 Debug Mode

If you are having trouble understanding why the interceptor is not working as expected, enable the debug mode. It will log helpful messages to the console with the [Auth-Queue] prefix.

applyAuthTokenInterceptor(apiClient, {
  // ...
  debug: true, // Logs: 🚨 401 Detected -> 🔄 Refreshing -> ✅ Success
});

⚙️ API Reference applyAuthTokenInterceptor(axiosInstance, config)

| Option | Type | Required | Description | | :----------------------- | :---------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------------- | | requestRefresh | (token) => Promise<AuthTokens> | ✅ | Core logic. Calls your backend to refresh tokens and must return AuthTokens. | | onSuccess | (tokens) => void | ✅ | Triggered when token refresh succeeds. Use this to persist new tokens. | | onFailure | (error) => void | ✅ | Triggered when token refresh fails. Use this to log out the user. | | getRefreshToken | () => string | ❌ | Retrieves the refresh token from storage before calling requestRefresh. | | checkTokenIsValid | () => Promise<boolean> | ❌ | 🔒 v2.0+ Required for cross-tab sync. Checks if a valid token already exists before refreshing. | | headerTokenHandler | (config) => void \| Promise<void> | ❌ | Async hook to attach the token to the request headers before each request. | | attachTokenToRequest | (config, token) => void | ❌ | Custom logic to attach the refreshed token to the retried request. Default: Authorization: Bearer {token} | | statusCodes | number[] | ❌ | HTTP status codes that trigger the refresh flow. Default: [401] | | refreshTimeout | number | ❌ | Maximum time (ms) to wait for the refresh request. Default: 30000 | | debug | boolean | ❌ | Enables colorful debug logs in the console. Default: false |

🤝 Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.