Search code examples
laravelvue.jses6-promiseaxioslaravel-passport

Axios Reponse Interceptor : unable to handle an expired refresh_token (401)


I have the following interceptor on my axios reponse :

window.axios.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    let errorResponse = error.response;
    if (errorResponse.status === 401 && errorResponse.config && !errorResponse.config.__isRetryRequest) {
      return this._getAuthToken()
        .then(response => {
          this.setToken(response.data.access_token, response.data.refresh_token);
          errorResponse.config.__isRetryRequest = true;
          errorResponse.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
          return window.axios(errorResponse.config);
        }).catch(error => {
          return Promise.reject(error);
        });
    }
    return Promise.reject(error);
  }
);

The _getAuthToken method is :

_getAuthToken() {
    if (!this.authTokenRequest) {
        this.authTokenRequest = window.axios.post('/api/refresh_token', {
            'refresh_token': localStorage.getItem('refresh_token')
        });
        this.authTokenRequest.then(response => {
            this.authTokenRequest = null;
        }).catch(error => {
            this.authTokenRequest = null;
        });
    }

    return this.authTokenRequest;
}

The code is heavily inspired by https://github.com/axios/axios/issues/266#issuecomment-335420598.

Summary : when the user makes a call to the API and if his access_token has expired (a 401 code is returned by the API) the app calls the /api/refresh_token endpoint to get a new access_token. If the refresh_token is still valid when making this call, everything works fine : I get a new access_token and a new refresh_token and the initial API call requested by the user is made again and returned correctly.

The problem occurs when the refresh_token has also expired.
In that case, the call to /api/refresh_token returns a 401 and nothing happens. I tried several things but I'm unable to detect that in order to redirect the user to the login page of the app.
I found that in that case the if (!this.authTokenRequest) statement inside the _getAuthToken method returns a pending Promise that is never resolved. I don't understand why this is a Promise. In my opinion it should be null...

I'm a newbie with Promises so I may be missing something ! Thanks for any help !

EDIT :

I may have found a way much simpler to handle this : use axios.interceptors.response.eject() to disable the interceptor when I call the /api/refresh_token endpoint, and re-enable it after.

The code :

createAxiosResponseInterceptor() {
        this.axiosResponseInterceptor = window.axios.interceptors.response.use(
            response => {
                return response;
            },
            error => {
                let errorResponse = error.response;
                if (errorResponse.status === 401) {
                    window.axios.interceptors.response.eject(this.axiosResponseInterceptor);
                    return window.axios.post('/api/refresh_token', {
                        'refresh_token': this._getToken('refresh_token')
                    }).then(response => {
                        this.setToken(response.data.access_token, response.data.refresh_token);
                        errorResponse.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
                        this.createAxiosResponseInterceptor();
                        return window.axios(errorResponse.config);
                    }).catch(error => {
                        this.destroyToken();
                        this.createAxiosResponseInterceptor();
                        this.router.push('/login');
                        return Promise.reject(error);
                    });
                }
                return Promise.reject(error);
            }
        );
    },

Does it looks good or bad ? Any advice or comment appreciated.


Solution

  • Your last solution looks not bad. I would come up with the similar implementation as you if I were in the same situation.

    I found that in that case the if (!this.authTokenRequest) statement inside the _getAuthToken method returns a pending Promise that is never resolved. I don't understand why this is a Promise. In my opinion it should be null...

    That's because this.authTokenRequest in the code was just assigned the Promise created from window.axios.post. Promise is an object handling kind of lazy evaluation, so the process you implement in then is not executed until the Promise was resolved.

    JavaScript provides us with Promise object as kind of asynchronous event handlers which enables us to implement process as then chain which is going to be executed in respond with the result of asynchronous result. HTTP requests are always inpredictable, because HTTP request sometimes consumes much more time we expect, and also sometimes not. Promise is always used when we use HTTP request in order to handle the asynchronous response of it with event handlers.

    In ES2015 syntax, you can implement functions with async/await syntax to hanle Promise objects as it looks synchronous.