Axios Interceptors are able to retry the initial request and gain access to the original promise

An implemented interceptor is set up to catch 401 errors in case the access token expires. When it does expire, it attempts to use the refresh token to obtain a new access token. Any other calls made during this process are queued until the access token is validated.

Everything is functioning smoothly with this setup. However, when processing the queue using Axios(originalRequest), the initially attached promises are not being executed. An example of this issue can be seen below.

Current interceptor code:

Axios.interceptors.response.use(
  response => response,
  (error) => {
    const status = error.response ? error.response.status : null
    const originalRequest = error.config

    if (status === 401) {
      if (!store.state.auth.isRefreshing) {
        store.dispatch('auth/refresh')
      }

      const retryOrigReq = store.dispatch('auth/subscribe', token => {
        originalRequest.headers['Authorization'] = 'Bearer ' + token
        Axios(originalRequest)
      })

      return retryOrigReq
    } else {
      return Promise.reject(error)
    }
  }
)

Refresh Method (Utilizing the refresh token to acquire a new access token)

refresh ({ commit }) {
  commit(types.REFRESHING, true)
  Vue.$http.post('/login/refresh', {
    refresh_token: store.getters['auth/refreshToken']
  }).then(response => {
    if (response.status === 401) {
      store.dispatch('auth/reset')
      store.dispatch('app/error', 'You have been logged out.')
    } else {
      commit(types.AUTH, {
        access_token: response.data.access_token,
        refresh_token: response.data.refresh_token
      })
      store.dispatch('auth/refreshed', response.data.access_token)
    }
  }).catch(() => {
    store.dispatch('auth/reset')
    store.dispatch('app/error', 'You have been logged out.')
  })
},

Subscribe method in auth/actions module:

subscribe ({ commit }, request) {
  commit(types.SUBSCRIBEREFRESH, request)
  return request
},

Along with the Mutation:

[SUBSCRIBEREFRESH] (state, request) {
  state.refreshSubscribers.push(request)
},

Here is an example action:

Vue.$http.get('/users/' + rootState.auth.user.id + '/tasks').then(response => {
  if (response && response.data) {
    commit(types.NOTIFICATIONS, response.data || [])
  }
})

If this request was added to the queue because the refresh token had to fetch a new token, I would like to include the original then() function:

const retryOrigReq = store.dispatch('auth/subscribe', token => {
originalRequest.headers['Authorization'] = 'Bearer ' + token
// I would like to attach the original .then() here as it contained critical functions to execute after the request completion, typically involving mutating the store.
Axios(originalRequest).then(//if then present attache here)
})

Once the access token has been refreshed, the queue of requests is processed:

refreshed ({ commit }, token) {
  commit(types.REFRESHING, false)
  store.state.auth.refreshSubscribers.map(cb => cb(token))
  commit(types.CLEARSUBSCRIBERS)
},

Answer №1

Latest Update: February 13, 2019

Due to the growing interest in this subject, a new package called axios-auth-refresh has been developed to assist in implementing the functionality discussed here.


The crucial aspect is ensuring the correct Promise object is returned for proper chaining using .then(). Leveraging Vuex's state can facilitate this process. By setting the refreshing state to true during a refresh call, and identifying the pending refreshing call, subsequent use of .then() will always be linked with the appropriate Promise object for execution upon completion. This approach eliminates the need for an additional queue to manage calls awaiting token refresh.

function refreshToken(store) {
    if (store.state.auth.isRefreshing) {
        return store.state.auth.refreshingCall;
    }
    store.commit('auth/setRefreshingState', true);
    const refreshingCall = Axios.get('get token').then(({ data: { token } }) => {
        store.commit('auth/setToken', token)
        store.commit('auth/setRefreshingState', false);
        store.commit('auth/setRefreshingCall', undefined);
        return Promise.resolve(true);
    });
    store.commit('auth/setRefreshingCall', refreshingCall);
    return refreshingCall;
}

This function will either return an existing request as a Promise or create a new one, saving it for other calls. Your interceptor setup would then resemble the following example.

Axios.interceptors.response.use(response => response, error => {
    const status = error.response ? error.response.status : null

    if (status === 401) {

        return refreshToken(store).then(_ => {
            error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
            error.config.baseURL = undefined;
            return Axios.request(error.config);
        });
    }

    return Promise.reject(error);
});

This configuration enables the execution of all pending requests simultaneously, without requiring any querying.


To ensure pending requests are executed in the order they were initiated, you can pass a callback as the second parameter to the refreshToken() function, like so.

function refreshToken(store, cb) {
    if (store.state.auth.isRefreshing) {
        const chained = store.state.auth.refreshingCall.then(cb);
        store.commit('auth/setRefreshingCall', chained);
        return chained;
    }
    store.commit('auth/setRefreshingState', true);
    const refreshingCall = Axios.get('get token').then(({ data: { token } }) => {
        store.commit('auth/setToken', token)
        store.commit('auth/setRefreshingState', false);
        store.commit('auth/setRefreshingCall', undefined);
        return Promise.resolve(token);
    }).then(cb);
    store.commit('auth/setRefreshingCall', refreshingCall);
    return refreshingCall;
}

And the updated interceptor:

Axios.interceptors.response.use(response => response, error => {
    const status = error.response ? error.response.status : null

    if (status === 401) {

        return refreshToken(store, _ => {
            error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
            error.config.baseURL = undefined;
            return Axios.request(error.config);
        });
    }

    return Promise.reject(error);
});

The feasibility of the second example has not been tested yet, but it should provide some insights into potential solutions.

View working demo of the first example - Please note that due to mock requests and the temporary nature of the service used in the demo, functionality may cease after some time. However, the code remains intact.

Reference: Interceptors - strategies to prevent intercepted messages from resolving as errors

Answer №2

To streamline the process, a single interceptor can handle this:

let _refreshToken = '';
let _authorizing: Promise<void> | null = null;
const HEADER_NAME = 'Authorization';

axios.interceptors.response.use(undefined, async (error: AxiosError) => {
    if(error.response?.status !== 401) {
        return Promise.reject(error);
    }

    // initiate authorization process
    _authorizing ??= (_refreshToken ? refresh : authorize)()
        .finally(() => _authorizing = null)
        .catch(error => Promise.reject(error));

    const originalRequestConfig = error.config;
    delete originalRequestConfig.headers[HEADER_NAME]; // use from defaults

    // wait for authorization completion before sending original requests
    return _authorizing.then(() => axios.request(originalRequestConfig));
});

The rest involves application-specific code:

  • Login to API
  • Save/load authentication data to/from storage
  • Refresh token

View the full example.

Answer №3

Why not experiment with a different approach like this?

This implementation involves using AXIOS interceptors in both request and response directions. In the outgoing direction, the Authorization header is set. For incoming responses, if an error occurs, a promise is returned to be resolved by AXIOS. The promise evaluates the error - refreshing the token if it's a 401 error encountered for the first time (not during retry). Otherwise, the original error is thrown. The refreshToken() method utilizes AWS Cognito, but any suitable alternative can be used. Two callbacks are defined for refreshToken():

  1. If the token refresh is successful, the AXIOS request is retried with updated configuration including the fresh token and a retry flag to prevent endless cycles of API responding with 401 errors. Resolving or rejecting the new promise must be done through passing resolve and reject arguments to AXIOS.

  2. If token refresh fails, the promise is rejected instead of just throwing an error to accommodate potential try/catch blocks within AWS Cognito callback.


Vue.prototype.$axios = axios.create(
  {
    headers:
      {
        'Content-Type': 'application/json',
      },
    baseURL: process.env.API_URL
  }
);

// Request Interceptor
Vue.prototype.$axios.interceptors.request.use(
  config => 
  {
    events.$emit('show_spin');
    let token = getTokenID();
    if(token && token.length) config.headers['Authorization'] = token;
    return config;
  },
  error => 
  {
    events.$emit('hide_spin');
    if (error.status === 401) VueRouter.push('/login'); // potentially redundant
    else throw error;
  }
);

// Response Interceptor
Vue.prototype.$axios.interceptors.response.use(
  response => 
  {
    events.$emit('hide_spin');
    return response;
  },
  error => 
  {
    events.$emit('hide_spin');
    return new Promise(function(resolve,reject) 
    {
      if (error.config && error.response && error.response.status === 401 && !error.config.__isRetry) 
      {
        myVue.refreshToken(function() 
        {
          error.config.__isRetry = true;
          error.config.headers['Authorization'] = getTokenID();
          myVue.$axios(error.config).then(resolve,reject);
        }, function(flag) 
        {
          if(process.env.NODE_ENV === 'development') console.log('Could not refresh token');
          if(getUserID()) myVue.showFailed('Could not refresh the Authorization Token');
          reject(flag);
        });
      } 
      else throw error;
    });
  }
); 

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Challenges encountered when making POST requests with redux-saga

Within my application, I am making a post request using Redux Saga middleware. Below is the code snippet relevant to my post request: function* postNewMessage(newMessage) { console.log(newMessage) const {var1, var2} = newMessage; try { ...

Error: The value of 'id' cannot be assigned to an undefined property

Recently, I've been delving into learning JS Express and decided to create a basic solution to handle GET / DELETE / POST / PUT requests. Everything was running smoothly until I encountered an issue with the POST router. Below is the code snippet for ...

Database with individual queries in each row

I'm in search of a tutorial that can guide me on creating an effective table using AJAX. For instance, I have one table containing user information and another table with records for each user. Users Table UserID | Name =======|=========== 000001 | K ...

Effortless document uploading and visualization

I am currently utilizing the express-fileupload package for image uploads. The uploaded images are stored in my local directory. My goal is to include the filename in the MongoDB database if possible and then display the image on the frontend of my applica ...

A guide on seamlessly transitioning from a mobile website to the corresponding native app

I am currently working on a mobile website project. This website is built using basic HTML and is accessed through a URL on a web browser, not as a native app or through PhoneGap. The client has requested links to their Facebook, Pinterest, YouTube, Twitt ...

Is there a way for me to extend an absolute div to the full width of its grandparent when it is within an absolute parent div?

Here is a structured example of my HTML and CSS: <div class="grandparent"> <div class="row">...</div> <div class="absolute-parent"> <div class="absolute-child">...</div> ...

Generating a visual preview of a .docx file using NodeJS

Looking to create an image preview of a word docx file, similar to Google Drive's functionality (refer to the image above). The process involves uploading a docx file which is then sent to the backend. Subsequently, the backend captures an image of th ...

Send the parameter to the compile method within the directive

I am currently working on creating a generic field and utilizing a directive for it. In my HTML code, I have defined the following: <div def-field="name"></div> <div def-field="surname"></div> <div def-field="children"></d ...

At runtime, the array inexplicably becomes null

Having recently ventured into the world of Ionic framework development, I have encountered a puzzling issue. At runtime, an array inexplicably gets nulled and I am struggling to pinpoint the root cause. export interface Days { name:string; } @Compon ...

What is causing my radio button event to activate when a separate radio button is selected?

I have implemented the code below to display "pers" and hide "bus," and vice versa. JQuery: $('input[name="perorbus"]').click(function() { if ($(this).attr('id') == 'bus') { $('#showbus&apo ...

Creating a basic JSON array using the push method

When adding comments to a blog post using the line: blogpost.comments.push({ username: "fred", comment: "Great"}); The JSON structure of the comments section will look like this: "comments":[{"0":{"username":"jim","comment":"Good",},"1":{"username":"fre ...

JavaScript does not recognize jsPDF

After importing the jsPDF library, I attempted to export to PDF but encountered a JavaScript error stating that jsPDF is not defined. I tried various solutions from similar posts but none of them seemed to work for me. You can find the fiddle here: https ...

The Vue Component Test has failed due to the inability to mount the component: the template or render function is undefined

While creating a test for a Vue component using Mocha, I encountered a warning that I cannot seem to resolve: [Vue warn]: Failed to mount component: template or render function not defined. Despite my research, it appears that most instances of this warn ...

The DataTable bootstrap is throwing an error when trying to access the property aDataSort

I am currently learning about bootstrap and working on a project that involves displaying data in a DataTable. However, I encountered an error that says Cannot read property aDataSort of undefined Feel free to make edits to my code if there are any mistak ...

The Node.js timestamp is later than the current date of the client

When storing data in MongoDB with the recording date, I utilize new Date() in Node.js and return that date with an AJAX response. To calculate the elapsed time from when the data was stored in MongoDB, I create a new date on the client-side. Then, I determ ...

Importing Next.js with variables or conditionally importing the module

import { keyFeatures } from 'common/data/AppClassic'; I am completely new to using Next.js and templates. Despite that, I have successfully integrated i18n into my project without much difficulty. However, I now face the challenge of wanting to ...

The Javascript Image() function fails to load or render

I am currently in the process of developing a basic Sprite class for implementation in a canvas game. However, I am encountering an issue where the image specified during the creation of a new instance of the class does not trigger the onload or onerror ev ...

Combining various Google calendar feeds into a single JSON object using JavaScript

I'm currently in the process of integrating JSON feeds from multiple Google calendars to organize upcoming events and showcase the next X number of events in an "Upcoming Events" list. While I initially achieved this using Yahoo! Pipes, I aim to elim ...

Automatic capitalization feature for input fields implemented with AngularJS

I have integrated a directive into my input field to validate a license key against a server-side API. While this functionality works well, I also want the license key to automatically add hyphens and appear in capital letters. For example, when a user in ...

Json error message displayed

I am currently learning javascript and working on creating a JSON code for a website. However, I am facing some error messages related to style, even though I have used style in the code. The specific error messages are as follows: 1: Bad template.json - E ...