Using axios interceptors in Frontend

Using axios interceptors in Frontend
Photo by Annie Spratt / Unsplash

Axios is the go-to library for managing HTTP requests in web development. While many developers have started using the native fetch API, if your requirements are more complex and you need a reliable solution, Axios remains the top choice. It offers a range of built-in features that are either challenging or not feasible to achieve with fetch.

Interceptors are such a feature in Axios that allows us to intercept HTTP requests or responses, making it possible to implement useful patterns that enhance the user experience. They function like middleware, being triggered before Axios resolves the request or response promise with .then() or .catch(). As you might expect, Axios provides two types of interceptors: one for requests and another for responses. Let's explore both types, using two common patterns in modern web development.

Before that, here is the basic structure of interceptors. Ref - Axios documentation

// request interceptor
axios.interceptors.request.use(function (config) {
    // Do something before request is sent
    return config;
  }, function (error) {
    // Do something with request error
    return Promise.reject(error);
  });

// response interceptor
axios.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
  }, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
  });

Handling refresh token with request interceptor

When managing authentication with JWT tokens, it's common practice to use refresh tokens alongside access tokens to prevent users from needing to log in frequently. Since access tokens are short-lived for security reasons, they must be periodically refreshed using the refresh token. The Axios request interceptor is the ideal place to handle this process.

axios.interceptors.request.use(async function (config) {
    // suppose we stored auth tokens in localStorage at the time of login
    let { accessToken, expiresAt, refreshToken } = JSON.parse(localStorage.getItem("auth"));
    const hasAccessTokenExpired = new Date().getTime() >= expiresAt;
    if (hasAccessTokenExpired) {
        // refresh the access token
        try {
            const response = await axios.post("/api/auth/refresh-token", {
                refreshToken,
            });
            // store the new tokens in localStorage
            localStorage.setItem("auth", JSON.stringify(response.data));
            accessToken = response.data.accessToken;
        } catch (err) {
            throw new Error("Invalid refresh token!");
        }
    }
    config.headers.Authorization = `Bearer ${accessToken}`;
    return config;
},
(error) => {
    return Promise.reject(error);
});

Retry on error with response interceptor

A request may occasionally fail due to a temporary server issue, and retrying it could resolve the problem. In such cases, an automatic retry mechanism can be useful. However, it's important to ensure that retries are only attempted for errors that are likely to be recoverable, such as 408, 429, 500, 502, 503, and 504. Otherwise, it will be just a waste of resources.

axios.interceptors.response.use(
    function (response) {
        // if the request succeeded, simply return the response
        return response;
    },
    async function (error) {
        // if the request failed, retry once more
        const { config, response } = error;

        // Only retry for 429, 500, 502, 503 and 504 status codes
        if (!response || [429, 500, 502, 503, 504].includes(response.status)) {
            return Promise.reject(error);
        }

        // Avoid infinite retry loops
        if (config._retry) {
            return Promise.reject(error);
        }

        config._retry = true;

        try {
            // Retry the original request
            return await axios(config);
        } catch (retryError) {
            return Promise.reject(retryError);
        }
    }
);