Handling Axios 401 Responses for Token Refresh
This article explains how to implement automatic JSON Web Token (JWT) refreshes using Axios when receiving an HTTP 401 Unauthorized response. By leveraging Axios response interceptors, applications can seamlessly catch authentication errors, use a stored refresh token to obtain a new access token, update the failed request's headers, and retry the original operation without disrupting the user experience.
The Mechanism: Axios Response Interceptors
Axios provides interceptors that allow you to intercept requests or
responses before they are handled by then or
catch. When an API returns an HTTP 401 Unauthorized status,
it typically signifies that the current access token has expired. A
response interceptor can capture this specific error code and initiate a
refresh flow.
Step-by-Step Implementation Flow
- Catch the 401 Error: The response interceptor
checks if
error.response.status === 401. - Prevent Infinite Loops: Add a custom flag (such as
_retry) to the original request configuration to ensure the interceptor does not repeatedly retry a request if the refresh itself fails. - Request a New Token: Send a request to the authentication endpoint using the refresh token.
- Update Authorization Headers: Store the new access
token and update both the default Axios headers and the original
request's
Authorizationheader. - Retry the Original Request: Pass the modified
original request configuration back to
axios(originalRequest).
Code Implementation
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'https://api.example.com',
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to attach access token
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 to handle 401 and refresh token
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Check if the error is 401 and that this request hasn't been retried yet
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refreshToken');
// Request a new access token
const response = await axios.post('https://api.example.com/auth/refresh', {
token: refreshToken,
});
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
// Update headers with new token
apiClient.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
// Retry the original request
return apiClient(originalRequest);
} catch (refreshError) {
// If refresh fails, log out the user and clear storage
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default apiClient;Handling Concurrent Requests
When multiple API requests fail simultaneously with a 401 error, triggering a refresh for each request can cause race conditions or invalidate tokens prematurely. To handle this, maintain a queue of failed requests and a boolean flag indicating if a refresh is currently in progress:
- Flag Active Refresh: If a refresh is in progress, push the failed request's resolver and rejecter into a callback queue instead of firing another refresh request.
- Process the Queue: Once the single refresh request resolves successfully, iterate through the queued requests, update their authorization headers, and execute them.
- Reject the Queue on Failure: If the refresh request fails, reject all requests in the queue and redirect the user to the login page.