Axios response interceptor for refreshing token keeps firing - javascript

The logic: On every request there's a JWT Authorization header that authenticates the user. If that expires, there's a cookie endpoint in place ready to refresh the JWT.
I am using axios and interceptor response to check if the client gets a 401 to try and refresh the JWT. The cookie may be valid or not.
The problem is that the interceptor to refresh the JWT never stops firing, and I think I have something wrong with the synchronization of the requests. Below is my code:
function refreshToken(dispatch) {
return new Promise((resolve, reject) => {
instance.put('/auth').then((response) => {
dispatch({ type: "UPDATE_AUTH", payload: response.data });
resolve(response);
})
.catch((error) => {
reject(error);
});
});
}
instance.interceptors.response.use(
response => {
return response;
},
err => {
const error = err.response;
if (error.status === 401 && error.config && !error.config._retry) {
error.config._retry = true;
return refreshToken(dispatch).then((resp) => {
return instance(error.config);
})
.catch((e) => {
return Promise.reject(e);
});
}
return Promise.reject(error);
}
);

If you have more then one parallel requests, refresh the JWT will be equal to the number of requests. Try to send /auth only first time and prevent next requests. You can use localStorage for this task.
let requests = [];
instance.interceptors.response.use(
response => {
return response;
},
err => {
const error = err.response;
if (error.status === 401 && error.config && !error.config._retry) {
requests.push(error.config);
if (!localStorage.getItem('refresh')) {
localStorage.setItem('refresh', 'done');
error.config._retry = true;
return refreshToken(dispatch).then((resp) => {
localStorage.removeItem('refresh');
const token = getAccessToken();
requests.map(req => {
req.headers.Authorization = `Bearer ${token}`;
return instance(req)
})
})
.catch((e) => {
localStorage.removeItem('refresh');
return Promise.reject(e);
});
}
} else {
requests = [];
}
return Promise.reject(error);
}
);

Related

Return Response When First request failed And Try In Second Request

I try to explain the problem.in App.js I have Function getUser .when call this function.in first request get 401 error . For this reason in axios.interceptors.response I receive error 401.At this time, I receive a token and repeat my request again.And it is done successfully.But not return response in Function getUser.
I have hook for authentication and send request.
import React from "react";
import axios from "axios";
const API_URL = "http://127.0.0.1:4000/api/";
function useJWT() {
axios.interceptors.request.use(
(request) => {
request.headers.common["Accept"] = "application/json";
console.log("request Send ");
return request;
},
(error) => {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
(response) => {
console.log("answer = ", response);
return response;
},
(error) => {
if (error?.response?.status) {
switch (error.response.status) {
case 401:
refreshToken().then((responseTwo) => {
return
sendPostRequest(
error.response.config.url
.split("/")
.findLast((item) => true)
.toString(),
error.response.config.data
);
});
break;
case 500:
// Actions for Error 500
throw error;
default:
console.error("from hook interceptor => ", error);
throw error;
}
} else {
// Occurs for axios error.message = 'Network Error'
throw error;
}
}
);
const refreshToken = () => {
const token = localStorage.getItem("refresh");
return axios
.post(API_URL + "token", {
token,
})
.then((response) => {
if (response.data.access) {
localStorage.setItem("access", response.data.access);
}
if (response.data.refresh) {
localStorage.setItem("refresh", response.data.refresh);
}
return response.data;
});
};
function login(email, password) {
return axios
.post(API_URL + "login", {
email,
password,
})
.then((response) => {
if (response.data.access) {
localStorage.setItem("access", response.data.access);
}
if (response.data.refresh) {
localStorage.setItem("refresh", response.data.refresh);
}
return response.data;
});
}
const sendPostRequest = (url, data) => {
console.log(300);
const token = localStorage.getItem("access");
axios.defaults.headers.common["jwt"] = token;
return axios.post(API_URL + url, {
data,
});
};
const logout = () => {
const token = localStorage.getItem("refresh");
return axios
.delete(API_URL + "logout", {
token,
})
.then((response) => {
localStorage.removeItem("access");
localStorage.removeItem("refresh");
});
};
return {
login,
logout,
refreshToken,
sendPostRequest,
};
}
export default useJWT;
In App.js ,I want to repeat the same request again if a 401 error is issued when I read the user information.
The request is successfully repeated but does not return the value.
When first request fail response is return equals null . and in catch when receive 401 error i am send second request but not return response.
I send request below code .
const getUser = () => {
console.log(12);
return sendPostRequest("user");
};
useEffect(() => {
let token = localStorage.getItem("access");
console.log("token = ", token);
if (token != null) {
//Here I have done simulation for 401 error
localStorage.setItem("access", "");
getUser()
.then((response) => {
console.log("response 1= ", response);
})
.catch((exception) => {
console.log("exception = ", exception.toString());
})
.then((response) => {
console.log("response 2= ", response);
});
} else {
navigate("/login");
}
}, []);
Best regards.
I didn't fully understand what exactly you want to do here.
But if you are looking to retry when 401 happens, you could use axios-retry to do it for you.
I'll pass the basics, but you can look more into what this does.
// First you need to create an axios instance
const axiosClient = axios.create({
baseURL: 'API_URL',
// not needed
timeout: 30000
});
// Then you need to add this to the axiosRetry lib
axiosRetry(axiosClient, {
retries: 3,
// Doesn't need to be this, it can be a number in ms
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
// You could do this way or try to implement your own
return error.response.status > 400
// something like this works too.
// error.response.status === 401 || error.response.status >= 500;
}
});
Just like in your code, we need to use interceptors if you want to avoid breaking your page, otherwise you can use try catch to catch any errors that may happen in a request.
// It could be something like this, like I said, it's not really needed.
axiosClient.interceptors.response.use(
(success) => success,
(err) => err
);
And finally, you could use the axiosClient directly since it now has your API_URL, calling it like this axiosClient.post('/user').
More or less that's it, you should just debug this code and see what is causing the return value to be null.
I would change these then/catch to be an async/await function, it would be more readable making your debugging easier.
axios-retry example if you didn't understand my explanation.
I find anwser for this question.
When error 401 occurs then create new Promise
I Wrote this code.
case 401:
return new Promise((resolve, reject) => {
refreshToken().then((responseTwo) => {
resolve(
sendPostRequest(
error.response.config.url
.split("/")
.findLast((item) => true)
.toString(),
error.response.config.data
)
);
});
});

Create a redux middleware retry function for expired sessions

In my app, I'm running every request trough an api middleware. I'm trying to create a middleware retry function, when requests with Authorization are happening and have an expired token. Here is my current API middleware:
const apiMiddleware = ({ dispatch }) => next => action => {
next(action);
// creating request data and params
const retryRequest = () => {
// refresh tokens with method: dispatch(getTokens());
// retry initial request
};
axios({
method,
url,
headers,
[dataOrParams]: data,
})
.then(({ data: apiData }) => {
dispatch(onSuccess(apiData));
})
.catch(error => {
if (withToken && error.response.status === 401) {
retryRequest();
}
return dispatch(apiError(label, error));
})
};
export default apiMiddleware;
When I call retryRequest() method the getTokens() request starts, but at the same time starts the initial requests, and Redux is not yet updated with the new refresh token, and the requests fail again, because getTokens() is not finished.
I understand that I'm doing it the wrong way, what other solutions I can try? So that first the request getTokens() is called and finished than the initial request can go on.
If you can make retryRequest() and the catch function async, you can use
if(withToken && error.reponse.status === 401)
await retryRequest();
return dispatch(apiError(label, error));
If you cant, just return the promise from retry request and
if(withToken && error.response.status === 401)
return retryRequest().then(_=> dispatch(apiError(label, error)))
return dispatch(apiError(label, error))
You could also use an axios interceptor, this is the one I use in my most recent projects
api.interceptors.response.use(
function (response) {
response.data = parseResponseData(response.data);
return response;
},
async function (error) {
if (!error.response)
Ant.message.error('Não foi possivel se conectar com o servidor');
else if (
error.response.status === 500 &&
window.location.pathname !== '/erro/500'
) {
if ((error.config.method as string).toLowerCase() === 'get')
navigate('/erro/500');
else
Ant.message.error(
'Desculpe, parece que algo deu errado no servidor.',
);
} else if (error.response.status === 401) {
let request = error.config;
const auth = localStorage.getItem('auth');
let refreshToken = auth && JSON.parse(auth)['refreshToken'];
var authService = new AuthService();
return await authService
.refresh(refreshToken)
.then(async (resp) => {
store.dispatch(login(resp.data));
let axiosInstance = axios.create();
intercept(axiosInstance);
return await axiosInstance.request(request);
})
.catch((e) => {
if (
window.location.pathname !== '/login' &&
(!e.response || e.response.status === 401)
)
navigate('/login');
});
}
return error.response;
},
);
It has worked pretty well so far.

Axios interceptor refresh token for multiple requests

I'll throw the http request because I'm calling the refresh token when it returns 401. After the refresh token response, I need to throw the previous request
SAMPLE
Logın -> — 1 hours later— —> call product —> 401 —> call refresh token —> call product
I try this link a link and look this link a link but doesn't work.
Catch the 401 error
setInterceptors = () => {
axios.interceptors.response.use(
response => {
return response;
},
err => {
return new Promise((resolve, reject) => {
if (err.response.status === 401 && err.config && !err.config.__isRetryRequest) {
const originalRequest = err.config;
this.emit('onAutoLogin', originalRequest);
}
// throw err;
});
}
);
};
Call my action
jwtService.on('onAutoLogin', originalRequest => {
jwtService
.signInWithToken()
.then(res => {
if (res.access_token) {
originalRequest.headers['Authorization'] = 'Bearer ' + res.access_token;
Axios.request(originalRequest).then(response => {
store.dispatch({
type: ** MY PROBLEM İS HERE **
payload: response.data
});
});
}
})
.catch(err => {
jwtService.setSession(null);
});
using this link I was able to solve the problem without triggering the redux store.
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(
response => {
return response;
},
err => {
const originalRequest = err.config;
if (err.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;
return new Promise(function(resolve, reject) {
axios
.post('/fooUrl/refreshToken', {
refreshToken: "fooToken"})
.then(({ data }) => {
axios.defaults.headers.common['Authorization'] = 'Bearer ' + data.fooToken;
originalRequest.headers['Authorization'] = 'Bearer ' + data.fooToken;
processQueue(null, data.fooToken);
resolve(axios(originalRequest));
})
.catch(err => {
processQueue(err, null);
store.dispatch(showMessage({ message: 'Expired Token' }));
reject(err);
})
.then(() => {
isRefreshing = false;
});
});
}
return Promise.reject(err);
}
);

Error with axios: TypeError: Cannot read property 'status' of undefined

Ok, I have an interceptors in my index.js to refresh the token if the status code is 401, this works fine, but in login page if I return another status code from the server, the messages errors in the page not working, because the axios interceptors not receive a 401.
But if receive a 401, the interceptors work fine.
This is a screenshot about that. It returns a 404 from the server if not found the user.
The error is related with Login.vue, but if I delete the axios.interceptors in my index.js the "status" in Login.vue it works fine.
Interceptors
axios.interceptors.response.use(response => {
return response;
}, error => {
if (error.response.status === undefined) {
return;
} else {
const code = error.response.status;
if (code === 401) {
localStorage.removeItem("token");
axios.get("token/refresh", {
params: {
correo: window.localStorage.getItem("email")
}
}).then(response => {
var refresh_token = response.data.token;
localStorage.setItem("token", refresh_token);
}).catch(error => {
const response = error.response;
console.log(response.data.errors);
})
return Promise.reject(error);
}
}
});
I tried in the interceptors, to use something like this:
if(error.response.status == undefined) return;
But it doesn't work.
Login Catch
.catch(error => {
this.loading = false;
if (error.response.status != null) {
switch (error.response.status) {
case 400:
this.alertError = true;
this.errorMessage = "No estás autorizado para acceder.";
this.loading = false;
break;
case 500:
this.alertError = true;
this.errorMessage =
"Hay un problema con el servidor, disculpa las molestias.";
this.loading = false;
break;
case 404:
this.alertError = true;
this.errorMessage = "Vuelve a ingresar tu contraseña";
break;
default:
this.alertError = true;
this.errorMessage =
"Hay un error interno en el servidor, intenta de nuevo más tarde";
}
}
})
How to handle this?
A response interceptor must return a response, a resolved promise or a rejected promise.
Your interceptor has logic paths that don't return anything, hence your problem.
You want something like this...
axios.interceptors.response.use(
(r) => r,
(error) => {
if (error.response.status === 401) {
localStorage.removeItem("token");
// make sure to return the new promise
return axios
.get("token/refresh", {
params: {
correo: window.localStorage.getItem("email"),
},
})
.then((response) => {
localStorage.setItem("token", response.data.token);
// now replay the original request
return axios.request(error.config);
});
}
// not a 401, simply fail the response
return Promise.reject(error);
}
);
From your implementation, you are trying to refresh the token. What you can do is to shift the refresh token handling to the server side. So If the token expires the server will send the token in the header and inside the axios interceptors write a code as follows to update the local storage when the header contains auth token.
Below a sample implementation from my side project
export default () => {
const authTokenJson = localStorage.getItem('auth');
let axiosInstance = null;
if (authTokenJson) {
const tokens = JSON.parse(authTokenJson);
axiosInstance = axios.create({
headers: {
'authorization': `Bearer ${tokens.token}`,
'refresh-token': `${tokens.refreshToken}`
}
});
} else {
axiosInstance = axios.create();
}
axiosInstance.interceptors.response.use(
response => {
console.log(response);
const tokens = {
token: response.headers['authorization'],
refreshToken: response.headers['refresh-token']
};
if (tokens.token && tokens.refreshToken) {
setTokens(JSON.stringify(tokens));
}
return response;
},
err => {
if (err.response.status === 401) {
EventBus.$emit('errors:401');
}
return Promise.reject(err);
}
);
return axiosInstance;
};

React + Redux update accessToken by refreshToken and re-send all crash dispatch with new accessToken

I have this code to add accessToken to all request:
axios.interceptors.request.use(
function(config) {
config.params = config.params || {};
config.params.access_token = localStorage.getItem("access_token");
return config;
},
function(error) {
// Do something with request error
return Promise.reject(error);
}
);
And I have interceptor to check all 401 errors
axios.interceptors.response.use(
function(response) {
return response;
},
function(error) {
if (error.response.status === 401) {
if (error.response.data.error === "invalid_token") {
history.push("/login");
} else if (error.response.data.error === "token_expired") {
//self.props.dispatch(OauthAction.tokenExpired());
}
}
return Promise.reject(error);
}
);
How can I get new access token by refreshToken, when token expired and resend all crash dispatch with new token?
For example, when this request is crash, I need to get new token and my function must again send request for update data on page:
export function getNews() {
return (dispatch) => {
axios.get(Config.DOMAIN + "news").then((response) => {
dispatch(setNews(response.data));
});
};
}

Categories