Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interceptors - how to prevent intercepted messages from resolving as error #266

Closed
dmt0 opened this issue Mar 14, 2016 · 33 comments
Closed

Comments

@dmt0
Copy link

dmt0 commented Mar 14, 2016

I'm trying to make an interceptor for 401 responses that result from expired token. Upon interception I want to login and retry the requests with the new token. My problem is that login is also done asynchronously, so by the time the retry happens, the original promises reject. Is there a way around that? Here's my code:

axios.interceptors.response.use(undefined, err => {
  if (err.status === 401 && err.config && !err.config.__isRetryRequest) {
    refreshLogin(getRefreshToken(),
      success => {
        setTokens(success.access_token, success.refresh_token)
        err.config.__isRetryRequest = true
        err.config.headers.Authorization = 'Bearer ' + getAccessToken()
        axios(err.config)
      },
      error => { console.log('Refresh login error: ', error) }
    )
  }
})
@mzabriskie
Copy link
Member

Sorry for the delay, I am just seeing this issue.

You need to make a return statement from your interceptor which will keep the Promise alive. More or less change your refreshLogin to return a Promise and then return that from your interceptor.

Or if you can't refactor refreshLogin you can wrap it in a Promise.

axios.interceptors.response.use(undefined, err => {
  let res = err.response;
  if (res.status === 401 && res.config && !res.config.__isRetryRequest) {
    return new Promise((resolve, reject) => {
      refreshLogin(getRefreshToken(),
        success => {
          setTokens(success.access_token, success.refresh_token)
          err.config.__isRetryRequest = true
          err.config.headers.Authorization = 'Bearer ' + getAccessToken()
          resolve(axios(err.config))
        },
        error => {
          console.log('Refresh login error: ', error)
          reject(error)
        }
      )
    });
  }
})

@dmt0
Copy link
Author

dmt0 commented Jul 18, 2016

Nope, doesn't do it.

The line "let res = err.response" doesn't work - there's no response in err.

If I leave it out, than the success gets triggered - so login happens, but the original request doesn't get retried.

@mzabriskie
Copy link
Member

What version of axios are you using?

@dmt0
Copy link
Author

dmt0 commented Jul 18, 2016

That was with 0.12.0.
Now that I updated to 0.13.1, err has become a string with call stack.

Error: Request failed with status code 401
at createError (eval at ...

@alexslade
Copy link

@dmt0 Did you get this working?

@dmt0
Copy link
Author

dmt0 commented Aug 2, 2016

@heeton Nope, been busy with other things. The solution posted didn't "just work". But it might have been due to some funny behavior in my backend. Will keep you posted...

@geocine
Copy link

geocine commented Oct 5, 2016

You could do something like this:

axios.interceptors.response.use(function (response) {
  return response;
}, function (error) {

  const originalRequest = error.config;

  if (error.response.status === 401 && !originalRequest._retry) {

    originalRequest._retry = true;

    const refreshToken = window.localStorage.getItem('refreshToken');
    return axios.post('http://localhost:8000/auth/refresh', { refreshToken })
      .then(({data}) => {
        window.localStorage.setItem('token', data.token);
        window.localStorage.setItem('refreshToken', data.refreshToken);
        axios.defaults.headers.common['Authorization'] = 'Bearer ' + data.token;
        originalRequest.headers['Authorization'] = 'Bearer ' + data.token;
        return axios(originalRequest);
      });
  }

  return Promise.reject(error);
});

@BrunoQuaresma
Copy link

Here, error.response return undefined in 0.14.0 version.

@geocine
Copy link

geocine commented Oct 5, 2016

may you try error.status please

@BrunoQuaresma
Copy link

axios.interceptors.response.use(undefined, error => {
      console.log(error.status)
      return Promise.reject(error)
    })

The console.log returns undefined.

@geocine
Copy link

geocine commented Oct 5, 2016

I am pretty sure it should be error.response.status you could check out the error object if it has those properties. I don't know why it doesn't work on your end if you only have that isolated sample code similar to what you have posted above.

@sripri
Copy link

sripri commented Dec 14, 2016

Is this issue resolved? I am using 0.15.2 and running into this weird problem

@Calvin92
Copy link

@dmt0 The same with you... I am using 0.15.2 ... So, which version is okay for this ?

@rlambertsen
Copy link

rlambertsen commented Jan 11, 2017

I used @geocine example from Oct, 5th 2016. I am using Vue.js 2.0 my solution was to watch the token for changes in the Vuex if it changed then to update anything that changed. I.E. if someone wanted to like a post but the JWT was expired, it intercepts the request and runs it again once the JWT has been refreshed.

Vue.axios.interceptors.request.use((config) => {
    if (store.state.auth) {
      config.headers.common['Authorization'] = 'Bearer ' + store.state.token
    }
    return config
  }, function (error) {
    // Do something with request error
    return Promise.reject(error)
  })
Vue.axios.interceptors.response.use((response) => {
    return response
  }, function (error) {
    let originalRequest = error.config
    console.log(originalRequest)
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true
      Vue.axios.post('/users/token', null).then((data) => {
        store.dispatch('authfalse')
        store.dispatch('authtruth', data.data)
        originalRequest.headers['Authorization'] = 'Bearer ' + store.state.token
        return Vue.axios(originalRequest)
      })
    }
    // Do something with response error
    return Promise.reject(error)
  })

@vijaythecoder
Copy link

is this solved yet? there is no response in error. it gives undefined. I want to check if the status is 401 and then redirect to login.

@ametad
Copy link

ametad commented Feb 16, 2017

In the response interceptor rejection function, the error.response.status is returned correctly in the latest version (0.15.3).

@muhammadaser
Copy link

is this solved ? im using version 0.15.3 but error.response is undefined. i try to handle 401 error.
btw im using "Custom instance"

@rmoskal
Copy link

rmoskal commented Apr 4, 2017

I was able to adapt @rlambertsen example. Worked fine.

@dennis2k
Copy link

dennis2k commented Apr 10, 2017

This is also an issue for me - v.0.15.3 - the error is just a callstack error string

@Toilal
Copy link

Toilal commented Apr 11, 2017

it works here, i'm running axios v0.16.1.

The only thing to note is that if you have set baseURL in your configuration, you have to remove it from the error.config object before retrying the initial request.

this.responseInterceptor = this.axios.interceptors.response.use((response) => response, (error) => {
  let value = error.response;

  if (value.status === 401 && value.data.message === 'Expired JWT Token'
    && (!value.config || !value.config.renewToken)) {
    console.log('Token JWT expiré. Reconnexion ...');

    // renewToken performs authentication using username/password saved in sessionStorage/localStorage
    return this.renewToken().then(() => {
      error.config.baseURL = undefined; // baseURL is already present in url
      return this.axios.request(error.config);
    }).then((response) => {
      console.log('Reconnecté !');
      return response;
    });

  } else if (value.status === 401 && value.config && value.config.renewToken) {
    console.log('Echec de la reconnexion !');

    if (error.message) {
      error.message = 'Echec de la reconnexion ! ' + error.message + '.';
    } else {
      error.message = 'Echec de la reconnexion !';
    }

  } else if (value.status === 401) {
    console.log('Accès refusé.');
    // TODO: We could ask for username/password in a modal dialog...
  }

  return Promise.reject(error);
});

@vijaythecoder
Copy link

I looking for solution for the past 3 to 4 months on this and still nothing worked. I am using Laravel to return the error and browser is able to identify the 401 (Unauthorized) but no help with axios.
@Toilal I am trying for hours to fix this every time I see a comment which could help but no luck, today I tried again with "axios": "^0.16.1" no luck. Can someone please help on this? there are lot of people who need help with this.

The below code consoles Undefined. Simple the error doesn't have a response, no matter how you to try

axios.interceptors.response.use((response) => response, (error) => {
    let value = error.response
    console.log(value)
})

@Toilal
Copy link

Toilal commented Apr 11, 2017

Can you console.log(error) ? Maybe it's an internal error and fails before a response is actually read ?

1 similar comment
@Toilal
Copy link

Toilal commented Apr 11, 2017

Can you console.log(error) ? Maybe it's an internal error and fails before a response is actually read ?

@Toilal
Copy link

Toilal commented Apr 11, 2017

Maybe Laravel (or something else) intercept the error before axios, and reject the promise with another data type ? I'm using axios in a VueJS app, it's currently a tiny app as it's a new project.

Try to find out if you have another axios interceptor running before this one ?

@vijaythecoder
Copy link

Its a string
console.log(error)

Error: Network Error
    at createError (eval at <anonymous> (app.js:1611), <anonymous>:15:15)
    at XMLHttpRequest.handleError (eval at <anonymous> (app.js:1590), <anonymous>:87:14)

This is when I console error.config
image

@vijaythecoder
Copy link

Yes I am using the axios with Vuejs App, This is how I did

/* global API_URL */
window.axios = require('axios').create({
  baseURL: API_URL,
  timeout: false,
  params: {} // do not remove this, its added to add params later in the config
})

// Add a request interceptor
/* global window axios */
axios.interceptors.request.use(function (config) {
  /* global window Store */
  let token = Store.get('jwt.token')
  let location = Store.get('location')

  // console.log(location.id, location)
  if (token) {
    config.headers.common['Authorization'] = 'Bearer ' + token
  }
  // Append location id for every post/get request
  if (location) {
    if (config.data) {
      config.data.location_id = location.id
    }
    else if (config.params) {
      config.params.location_id = location.id
    }
  }

  return config
}, function (error) {
  // Do something with request error
  return Promise.reject(error)
})

axios.interceptors.response.use((response) => response, (error) => {
    console.log(error.config)
})

@vijaythecoder
Copy link

@Toilal I figured out the problem and not sure what could be the fix. If I remove
axios.interceptors.request part, it works great. Should we use both request and response in same call ? if yes do you know a work around ?

@Toilal
Copy link

Toilal commented Apr 11, 2017 via email

@vijaythecoder
Copy link

vijaythecoder commented Apr 11, 2017

I solved it, the 401 doesn't have cors. If any one using Laravel with Laravel Cors Package then here is the solution and you will have error.response available

fruitcake/laravel-cors#89

@felixbuenemann
Copy link

I had the problem, that error was undefined in a response interceptor and it was caused by not resolving the promise for config in a token refresh request interceptor, if the token refresh failed.

Faulty code in request interceptor:

axios.interceptors.request.use(config => this.dispatch('refreshAuthToken').then(() => {
  // do stuff to set Authorization header
  return Promise.resolve(config);
}))

Fixed code:

axios.interceptors.request.use(config => this.dispatch('refreshAuthToken').then(() => {
  // do stuff to set Authorization header
  return Promise.resolve(config);
// ensure we resolve config in error case
}), () => Promise.resolve(config));

@Jenan
Copy link

Jenan commented Oct 10, 2017

Based on this sample: #450 (comment)

I have used this version for refreshing token - as is described on post above. This interceptor is created for prevent refresh token repeatly if it is created more request and you probable want to call this operation only once.

This is typescript version but I think it is very similiar to js version.

export default class AuthorizationHelper {

	authTokenRequest: Promise<any>;

	getNewAccessToken() {

		const refreshToken = window.localStorage.getItem("refreshToken");

		if (!this.authTokenRequest) {
			this.authTokenRequest = this.refreshToken(refreshToken);
			this.authTokenRequest.then(response => {
				this.resetGetAccessTokenRequest();
			}).catch(error => {
				this.resetGetAccessTokenRequest();
			});
		}

		return this.authTokenRequest;
	}

	resetGetAccessTokenRequest() {
		this.authTokenRequest = null;
	}

	refreshToken(refreshToken: string): Promise<any> {

		return axios.post('/api/token/refresh',
			{
				refreshToken: refreshToken
			});
	}

	registerAxiosInterceptor() {
		axios.interceptors.response.use((response) => {
			return response;
		}, err => {
			const error = err.response;

			if (error.status === 401 && error.config && !error.config.__isRetryRequest) {

				return this.getNewAccessToken().then(response => {
					error.config.__isRetryRequest = true;

					//set new access token after refreshing it
					axios.defaults.headers.common["Authorization"] = `Bearer ${response.access_token}`;
					error.config.headers["Authorization"] = `Bearer ${response.access_token}`;

					return axios(error.config);
				}).catch(error => {

					//refreshing has failed => redirect to login
					//clear cookie (with logout action) and return to identityserver to new login
					//(window as any).location = "/account/logout";

					return Promise.reject(error);
				});
			}

			return Promise.reject(error);
		});
	}
}

@Godofbrowser
Copy link

Godofbrowser commented Jul 26, 2018

// for multiple requests
let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  })
  
  failedQueue = [];
}

axios.interceptors.response.use(function (response) {
  return response;
}, function (error) {

  const originalRequest = error.config;

  if (error.response.status === 401 && !originalRequest._retry) {
      
      if (isRefreshing) {
        return new Promise(function(resolve, reject) {
          failedQueue.push({resolve, reject})
        }).then(token => {
          originalRequest.headers['Authorization'] = 'Bearer ' + token;
          return axios(originalRequest);
        }).catch(err => {
          return Promise.reject(err);
        })
      }

    originalRequest._retry = true;
    isRefreshing = true;

    const refreshToken = window.localStorage.getItem('refreshToken');
    return new Promise(function (resolve, reject) {
       axios.post('http://localhost:8000/auth/refresh', { refreshToken })
        .then(({data}) => {
            window.localStorage.setItem('token', data.token);
            window.localStorage.setItem('refreshToken', data.refreshToken);
            axios.defaults.headers.common['Authorization'] = 'Bearer ' + data.token;
            originalRequest.headers['Authorization'] = 'Bearer ' + data.token;
            processQueue(null, data.token);
            resolve(axios(originalRequest));
        })
        .catch((err) => {
            processQueue(err, null);
            reject(err);
        })
        .then(() => { isRefreshing = false })
    })
  }

  return Promise.reject(error);
});

Demo project
Gist link

@felix2056
Copy link

What you see in Axios version 0.13.* error response the string returned by the toString method of the error object.

In the newer versions, If a response has been received from the server, the error object will contain the response property:
So you can do something like:

if (error.response) { console.log(error.response.data); console.log(error.response.status); console.log(error.response.headers); }

@axios axios locked and limited conversation to collaborators May 21, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests