Axios Interceptor Refresh Token Handling - Complete Code
Learn how to write an Axios interceptor to refresh JWT access tokens automatically. Full code example with request queuing for handling parallel requests.
Official Documentation Reference
For in-depth explanations, configuration options, and standards compliance, visit the official documentation:
MDN Web Docs - Fetch API →Ready to test this code?
Code Example
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) prom.reject(error);
else prom.resolve(token);
});
failedQueue = [];
};
apiClient.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers['Authorization'] = `Bearer ${token}`;
return apiClient(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const response = await axios.post('/auth/refresh', { token: getRefreshToken() });
const newToken = response.data.accessToken;
setAccessToken(newToken);
processQueue(null, newToken);
return apiClient(originalRequest);
} catch (err) {
processQueue(err, null);
handleLogout();
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);Overview
Axios Interceptor Refresh Token Handling - Complete Code
In client-side applications using JSON Web Tokens (JWT) for authentication, tokens are split into two parts: an Access Token (short-lived, e.g., 15 minutes) and a Refresh Token (long-lived, e.g., 7 days).
When the access token expires, a request returns a 401 Unauthorized status code. A robust application should intercept this 401 error, request a new access token in the background, queue any other outgoing API requests, and replay them with the new token seamlessly without logging out the user or forcing a manual refresh.
Implementation Code
Here is a complete, production-ready Axios interceptor implementation that handles parallel requests and queuing:
import axios from 'axios'; // Create base Axios instance const apiClient = axios.create({ baseURL: 'https://api.example.com/v1', headers: { 'Content-Type': 'application/json', }, }); // Track refresh token state let isRefreshing = false; let failedQueue = []; // Helper to process the queued requests const processQueue = (error, token = null) => { failedQueue.forEach((prom) => { if (error) { prom.reject(error); } else { prom.resolve(token); } }); failedQueue = []; }; // Request Interceptor: Attach the current Access Token to headers apiClient.interceptors.request.use( (config) => { const token = localStorage.getItem('accessToken'); if (token) { config.headers['Authorization'] = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // Response Interceptor: Intercept 401 errors and handle refresh apiClient.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; // Check if error status is 401 and request hasn't been retried yet if (error.response?.status === 401 && !originalRequest._retry) { if (isRefreshing) { // Queue requests if token refresh is already in progress return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }) .then((token) => { originalRequest.headers['Authorization'] = `Bearer ${token}`; return apiClient(originalRequest); }) .catch((err) => Promise.reject(err)); } originalRequest._retry = true; isRefreshing = true; const refreshToken = localStorage.getItem('refreshToken'); if (!refreshToken) { // Redirect to login if refresh token is missing window.location.href = '/login'; return Promise.reject(error); } try { // Call endpoint to get new access token const response = await axios.post('https://api.example.com/v1/auth/refresh', { token: refreshToken, }); const { accessToken: newAccessToken } = response.data; // Store new token localStorage.setItem('accessToken', newAccessToken); // Attach token to headers and replay the original request originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; // Process all queued requests processQueue(null, newAccessToken); return apiClient(originalRequest); } catch (refreshError) { // If refresh token expires as well, log out the user processQueue(refreshError, null); localStorage.clear(); window.location.href = '/login'; return Promise.reject(refreshError); } finally { isRefreshing = false; } } return Promise.reject(error); } ); export default apiClient;
How it Works
- First-attempt authorization: The request interceptor appends the active access token stored in
localStorageto all outgoing requests. - 401 Interception: If a request returns
401 Unauthorized, the response interceptor catches it. It flags the request as_retry = trueto prevent infinite loops. - Queuing parallel requests: If other API calls are fired while the token is refreshing (
isRefreshing = true), they are wrapped in a new Promise and pushed to thefailedQueuearray, pausing their execution. - Refreshing the token: The main request sends the refresh token to the authentication API. If successful, it stores the new access token and replays the original request.
- Replaying the queue: The
processQueuefunction runs, resolving all queued requests with the new token, which automatically replays them, completing the auth cycle transparently.
References
Frequently Asked Questions
Q: Why do we need a queue for failed requests?
A: If a page fires 5 API calls in parallel on mount, and the access token is expired, all 5 will return 401. Without a queue, the client would hit the /auth/refresh endpoint 5 times concurrently, which degrades performance and is rejected by most servers as rate limiting or token reuse abuse.
Q: Should I store tokens in localStorage?
A: While simple, storing tokens in localStorage is vulnerable to Cross-Site Scripting (XSS) attacks. For production apps, secure HttpOnly cookies are recommended as the safest storage mechanism for JWTs.

