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

  1. Catch the 401 Error: The response interceptor checks if error.response.status === 401.
  2. 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.
  3. Request a New Token: Send a request to the authentication endpoint using the refresh token.
  4. Update Authorization Headers: Store the new access token and update both the default Axios headers and the original request's Authorization header.
  5. 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: