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 Unauthorizedsimultaneously.
License
π 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.
This library fixes it by:
- Intercepting the first
401error. - Pausing all other requests in a Queue.
- Calling the "Refresh Token" β¦
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 Unauthorizedsimultaneously.
License
π 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.
This library fixes it by:
- Intercepting the first
401error. - Pausing all other requests in a Queue.
- Calling the "Refresh Token" API once.
- Retrying all paused requests with the new token.
π¦ Installation
npm install axios-auth-refresh-queue
# or
yarn add axios-auth-refresh-queue
π οΈ Usage
- 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, {
// 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;
- 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. MUST enable withCredentials for cookies to work
const apiClient = axios.create({
baseURL: "[https://api.your-backend.com](https://api.your-backend.com)",
withCredentials: true,
});
let accessTokenMemory = null;
applyAuthTokenInterceptor(apiClient, {
// β NO getRefreshToken function needed
// (Because the browser handles the cookie automatically)
// 2. Refresh Logic
requestRefresh: async () => {
// Just call the API. The browser sends the cookie automatically.
const response = await axios.post(
"[https://api.your-backend.com/auth/refresh](https://api.your-backend.com/auth/refresh)",
{},
{ withCredentials: true } // π Important
);
return {
accessToken: response.data.accessToken,
// No need to return refreshToken if it's set via Set-Cookie header
};
},
// 3. Update Memory & Headers
onSuccess: (newTokens) => {
// β οΈ Don't store in localStorage
accessTokenMemory = newTokens.accessToken;
// Update default header for future requests
apiClient.defaults.headers.common[
"Authorization"
] = `Bearer ${newTokens.accessToken}`;
// (Optional) Update your Redux/Zustand state here if needed
// store.dispatch(setToken(newTokens.accessToken));
},
onFailure: (error) => {
// Call logout to clear cookies on server
axios.post("/auth/logout");
window.location.href = "/login";
},
});
export default apiClient;
β³ 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) | Property | Type | Required | Description | |Data |Data |Data |Data | | requestRefresh | (token) => Promise | Yes | Your API call logic to get a new token. | | getRefreshToken| () => string | Yes | Function to retrieve the current refresh token from storage. | | onSuccess | (tokens) => void | Yes | Callback invoked when a new token is retrieved successfully. | | onFailure | (error) => void | Yes | Callback invoked when the refresh logic fails (user should be logged out). | | attachTokenToRequest | (req, token) => void | No | Custom function to attach the new token to the retried request headers. |
π€ Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.