JavaScript
    rest-api

    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.

    0 views
    Updated 7/6/2026

    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?

    Load this example into the app

    Code Example

    Copy and run
    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

    1. First-attempt authorization: The request interceptor appends the active access token stored in localStorage to all outgoing requests.
    2. 401 Interception: If a request returns 401 Unauthorized, the response interceptor catches it. It flags the request as _retry = true to prevent infinite loops.
    3. 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 the failedQueue array, pausing their execution.
    4. 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.
    5. Replaying the queue: The processQueue function 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.

    Related Topics

    javascript
    axios
    jwt
    interceptors
    authentication
    token-refresh

    Ready to test APIs like a pro?

    Apicurl is a free, powerful API testing tool.