Axios Retry Infinite Loop - reactjs

I am using an Axios interceptor (in React) to retry on 401 (when my access token expires). I want to limit to one retry but for some reason I'm unable to read the retried property I am defining.
This is the code I am using in the interceptor.
const responseIntercept = axios.interceptors.response.use(
(response) => response,
async (error) => {
const prevRequest = error?.config;
console.log(prevRequest);
console.log(prevRequest.retried);
if (error?.response?.status === 401 && !prevRequest?.retried) {
await new Promise(r => setTimeout(r, 1500)); // only here to delay the infinite retries
prevRequest.retried = true;
// log here returns true
const newAccessToken = await refresh();
prevRequest.headers['Authorization'] = newAccessToken;
return axios(prevRequest);
}
return Promise.reject(error);
}
);
For some reason, logging of prevRequest shows an object with the property retried, but the second log of .retried always logs 'undefined'. I assume this is the problem but I have no idea why I can see the property set but can't access it.
If I log prevRequest after adding the property, it does return true.
console log
Edit (solution): After taking bogdanoff's advice, this is the working solution I ended up with:
const NO_RETRY_HEADER = 'x-no-retry'
...
const responseIntercept = axiosPrivate.interceptors.response.use(
(response) => response,
async (error) => {
var prevRequest = error?.config;
if (error?.response?.status === 401 && prevRequest?.headers[NO_RETRY_HEADER] == null) {
// get new token, return error if refresh errors
try {
const newAccessToken = await refresh(controller.signal);
// retry with new token
prevRequest.headers[NO_RETRY_HEADER] = 'true';
prevRequest.headers['Authorization'] = newAccessToken;
return axiosPrivate(prevRequest);
} catch (error) {/* no-op */}
}
return Promise.reject(error);
}
);

I have been there recently, I used headers instead of modifying config.
const NO_RETRY_HEADER = 'x-no-retry'
const responseIntercept = axios.interceptors.response.use(undefined, async (error) => {
if (!axios.isCancel(error) && axios.isAxiosError(error) && error.response.status === 401) {
if (error.config.headers && error.config.headers[NO_RETRY_HEADER]) {
return Promise.reject(error)
}
error.config.headers ||= {}
error.config.headers[NO_RETRY_HEADER] = 'true' // string val only
const newAccessToken = await refresh()
error.config.headers['Authorization'] = newAccessToken
return axios(error.config)
}
return Promise.reject(error)
})

Related

How to stop React from finishing render when axios.interceptors.response handles the error?

I am working on a react app and I use tokens and refresh tokens for authentication. Whenever the backend returns a 401, the axios.interceptors.response picks it up and tries to refresh my token. If it succeeds, it will reinitiate the original call with the updated headers. See the code below:
// To avoid infinite loops on 401 responses
let refresh = false;
axios.interceptors.response.use(
(resp) => resp,
async (error) => {
if (error.response.status === 401 && !refresh) {
refresh = true;
const response = await axios.post(
"/api/auth/refresh",
{},
{ withCredentials: true }
);
if (response.status === 200) {
axios.defaults.headers.common[
"Authorization"
] = `Bearer ${response.data["accessToken"]}`;
return axios(error.config);
}
}
refresh = false;
return error.response;
}
);
This by itself works great, but not in combination with the code below in one of my components:
const [pages, setPages] = useState();
const [error, setError] = useState();
const navigate = useNavigate();
useEffect(() => {
async function fetchInfo() {
const response = await getMyPages();
if (response.status === 200) {
setPages(response.data);
}
else if (response.status === 401) {
setError(t("error.notAuthorized"));
navigate(`/login`, { replace: true });
}
// Any other error
else {
setError(t("error.unexpected"));
}
}
fetchInfo();
}, [t, navigate]);
// getMyPages function
export async function getMyPages() {
try {
const result = await axios.get(`/api/user/mypages`);
return result;
} catch (err) {
return err.response;
}
}
The problem is that the user is navigated to /login before the new request (with refreshed token) is made and finished. So when the new request finishes, I am not in the original component anymore and I can no longer update the pages state.
Any suggestions on how to handle this?
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const getMyPages = async () => {
try {
const response = await axios.get(`/api/user/mypages`, {
signal: controller.signal
});
isMounted && setPages(response.data);
} catch (err) {
navigate(`/login`, { replace: true });
}
}
getMyPages();
return () => {
isMounted = false;
controller.abort();
}
}, [])

How can I re-call API in case of axios intercepter error

I am developing react project using JWT authentication and write this code.
const service = axios.create({
baseURL: API_BASE_URL,
timeout: 60000
});
service.interceptors.response.use( (response) => {
return response.data
}, async (error) => {
const originalRequest = error.config;
// Remove token and redirect
if (error.response.status === 400 || error.response.status === 403) {
localStorage.removeItem(AUTH_TOKEN)
history.push(ENTRY_ROUTE)
window.location.reload();
}
// Unauthorized error token should refresh with access token
if (error.response.status === 401) {
const jwtToken = localStorage.getItem(AUTH_TOKEN)
const token = JSON.parse(jwtToken)
const refresh_token = token['refresh'];
const tokenParts = JSON.parse(atob(refresh_token.split('.')[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
if (tokenParts.exp > now) {
try {
const res = await service.post('/authentication/token/refresh/', {refresh: refresh_token});
localStorage.setItem(AUTH_TOKEN, JSON.stringify(res));
service.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
originalRequest.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
return service(originalRequest);
}
catch (error) {
}
}
else {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
}
}
return Promise.reject(error);
});
This is working well and have no errors, but I have something to ask. When the token is expired and some API is called, I get 401 status code to get access token with refresh token. And it returns the access token correctly.
But there is no way to re-call the failed API ( just called with expired access token ). That's why there are cases I could have no response from the backend ( Imagine, user clicks button, but no response, so he should click the button again to see the response. This is because of calling with new access token again )
This code is used in all parts of the project and I have many API callings, so it seems impossible to re-call failed API in each react components.
How can I fix this problem?
Best bet is to invoke axios again with the original request (with new token). I haven't tried setting up JWT server & executing this scenario, but something like below should work:
const service = axios.create({
baseURL: API_BASE_URL,
timeout: 60000,
});
service.interceptors.response.use(
(response) => {
return response.data;
},
async (error) => {
const originalRequest = error.config;
// Remove token and redirect
if (error.response.status === 400 || error.response.status === 403) {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
}
if (error.response.status != 401) return Promise.reject(error);
// Unauthorized error token should refresh with access token
const jwtToken = localStorage.getItem(AUTH_TOKEN);
const token = JSON.parse(jwtToken);
const refresh_token = token["refresh"];
const tokenParts = JSON.parse(atob(refresh_token.split(".")[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
if (tokenParts.exp > now) {
try {
const res = await service.post("/authentication/token/refresh/", {
refresh: refresh_token,
});
localStorage.setItem(AUTH_TOKEN, JSON.stringify(res));
service.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res["access"];
originalRequest.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res["access"];
axios
.request(originalRequest)
.then((response) => {
Promise.resolve(response);
})
.catch((err) => {
Promise.reject(err);
});
//return service(originalRequest);
} catch (error) {
Promise.reject(error);
}
} else {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
Promise.reject(error);
}
}
);
Hope this helps 👍
After some days of investigating my code, I finally found the answer.
Let me write my code and hope it will be helpful.
let refreshToken;
...
if (tokenParts.exp > now) {
try {
if (!refreshToken) {
refreshToken = service.post('/authentication/token/refresh/', {refresh: refresh_token}).then(token => {
refreshToken = null;
return token;
});
}
return refreshToken.then(res => {
localStorage.setItem(AUTH_TOKEN, JSON.stringify(res));
service.defaults.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
originalRequest.headers[TOKEN_PAYLOAD_KEY] = "JWT " + res['access'];
return service(originalRequest);
})
}
catch (error) {
}
}
else {
localStorage.removeItem(AUTH_TOKEN);
history.push(ENTRY_ROUTE);
window.location.reload();
}
This prevents multiple token refresh API request and surely made the failed API call again with new access token.

Not able to get the response code in react js axios

Here is my common method for post:
export const post = (url: string, param: any) => {
const CancelToken = axios.CancelToken; // axios cancel request
const source = CancelToken.source();
post.prototype.source = source;
return axios.post(url, qs.stringify(param), { cancelToken:
source.token }).then((resp) => resp);
};
Here is my post method:
const postMyMethod = async () => {
await postMd(params)
.then((response: any) => {
console.log(response) // in response not getting status code
})
};
Below is the error handling code, how to get the response status code(ex: 200, 400...):
axios.interceptors.response.use(
function (response) {
if (response && response.data && response.data.Code && response.data.Message) {
message.error(response.data.Message);
response.data = null;
}
return response;
},
function (error) {
if (error.response && error.response.data && error.response.data.Code && error.response.data.Message) {
message.error(error.response.data.Message);
} else {
message.error('Unknown error, please check your network ~');
}
return error;
}
);
Finally if I do:
console.log(response)
Getting: Error: Request failed with status code 400
How to get the status code to do the if condition in the postMyMethod()?
I want to do like this in the postMyMethod(). How to achieve this?
if(response.status === 200){
// do something
}
if (respone.status === 400){
// do something
}
The error is because you are not using a catch() block in your postMyMethod function. You should add it so it will handle any error response. It will look something like this:
const postMyMethod = async () => {
await postMd(params)
.then((response) => {
console.log(response)
}).catch((err) => {
console.log(err.response.statuscode);
});
};
If response code 400 is something specific you want to handle differently in your function, your catch() block will be:
const postMyMethod = async () => {
await postMd(params)
.then((response) => {
console.log(response)
}).catch((err) => {
if (err.response.statuscode == 400) {
console.log(err);
} else {
console.log("something else");
}
});
};
You can read more about the catch() method here.
Finally got it:
.then((response: any) => {
console.log(response.response.status);
console.log(response.response.data);
})
or need to add below code under function error > if condition
return error.response;
Now getting the response status and failure data.

Redux state is not uniform across all tabs

Im facing an issue now with my authentication system that is based on django-simplejwt and redux-saga.
Whenever i click on the log out button , the following saga is run:
export function* workerLogout(action) {
const r_token = localStorage.getItem('refresh_token');
yield call(() => axios.post('http://127.0.0.1:8000/api/blacklist/', {
"refresh_token": r_token
})) //<--- API end-point to blacklist the tokens
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token'); // <--- removing the tokens
yield call(() => axiosInstance.defaults.headers['Authorization'] = null ); //<--- setting the headers to null
yield put({ type: "LOGOUT_SUCCESS" }) //<---- This will go to the reducer to set the states to null
}
Which will then acccess the reducer here:
const authLogout = (state, action) => {
return updateObject(state, {
access_token: null,
refresh_token: null,
isAuthenticated: false,
group: {},
username: null,
});
}
This works and my redux tool bar shows that the state of the tab is indeed cleared:
isAuthenticated(pin):false
access_token(pin):null
refresh_token(pin):null
group(pin):
username(pin):null
My interceptor:
axiosInstance.interceptors.response.use(
response => {
return response
},async error => {
const originalRequest = error.config;
// Prevent infinite loops
if (error.response.status === 401 && originalRequest.url === '/token/refresh/') {
store.dispatch({type: "LOGOUT"});
return Promise.reject(error);
}
if (error.response.data.code === "token_not_valid" &&
error.response.status === 401 &&
error.response.statusText === "Unauthorized")
{
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken){
const tokenParts = JSON.parse(atob(refreshToken.split('.')[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
if (tokenParts.exp > now) {
console.log('access token is expired , attempting refresh')
try {
const response = await axiosInstance
.post('/token/refresh/', { refresh: refreshToken });
localStorage.setItem('access_token', response.data.access);
localStorage.setItem('refresh_token', response.data.refresh);
axiosInstance.defaults.headers['Authorization'] = "JWT " + response.data.access;
originalRequest.headers['Authorization'] = "JWT " + response.data.access;
axiosInstance.get('group/get/').then(groups => {
store.dispatch({type: "LOGIN_SUCCESS" , payload:[response.data.access, refreshToken, groups.data, jwt_decode(response.data.access).username]});
})
return axiosInstance(originalRequest);
}
catch (err) {
store.dispatch({type: "GET_ERRORS" , error : err.response.data})
}
}else{
store.dispatch({type: "LOGOUT"});
}
}
}
store.dispatch({type: "GET_ERRORS" , error : error.response.data})
return Promise.reject(error);
}
);
However , if let's say a second tab was open at the point of the logout , i can still access my server's resources from the second tab. Upon closer inspection of the redux tool bar , it seems the state of the second tab is not affected by the first tab.
Is this an expected behavior? If so how do i make it in synced across all tabs as this seems like a bad way to secure the application.
I would suggest to store your Accesstoken and refreshtoken in Browser's local storage. Clearing accesstoken from redux store will not impact the other tabs.
On your Login success you can store accesstoken as
static saveToken(token, refreshToken){
window.localStorage.setItem('token', token);
window.localStorage.setItem('refresh_token', refreshToken);
}
On your logout success you can just remove the token by
static clearAccessToken(){
window.localStorage.removeItem('token');
window.localStorage.removeItem('refresh_token');
}
In your request Interceptor you can access the accesstoken/refreshtoken by
static isAuthenticated(){
return window.localStorage.token != null;
}
static getAccessToken(){
return window.localStorage.token;
}
static getRefreshToken(){
return window.localStorage.refresh_token;
}
By doing this way when you logout from one tab it will logged out from your browser.
You can contorl the logout error in authInterceptor
axios.interceptors.response.use((response) => {
return response
}, (error) => {
const { config, response: { status } } = error;
const originalRequest = config;
if (status === 401) {
if(config.url.endsWith('oauth/token') || config.url.endsWith('users/logout')){
return Promise.reject(error);
}
if (!this.isAlreadyFetchingAccessToken) {
this.isAlreadyFetchingAccessToken = true;
var refreshTokenValue = AuthService.getRefreshToken();
refreshToken(refreshTokenValue).then((token) => {
this.isAlreadyFetchingAccessToken = false
if(token && token.data && token.data.access_token){
AuthService.saveToken(token.data.access_token, token.data.refresh_token);
this.onAccessTokenFetched(token.data.access_token);
}
}).catch(err => {
this.isAlreadyFetchingAccessToken = false;
this.subscribers = [];
this.history.push('/login');
return Promise.reject("Auth error");
})
}
const retryOriginalRequest = new Promise((resolve) => {
this.addSubscriber(access_token => {
originalRequest.headers.Authorization = 'Bearer ' + access_token
resolve(axios(originalRequest))
})
})
return retryOriginalRequest
}
console.log("Unknown error :" + error);
let errorMessage = '';
if(error.response && error.response.data) {
return Promise.reject(error.response.data);
} else {
return Promise.reject({
code: UNKNOWN_ERROR,
message: "Oops, Unable to complete the request"
});
}
})
This is my request interceptor
axios.interceptors.request.use(function (config) {
if(AuthService.isAuthenticated()){
if(config.url.startsWith(apiUrl) && !config.url.startsWith(OAUTH_URL)){
config.headers.Authorization = 'Bearer ' + window.localStorage.token;
}
}
return config;
});
In the above code I am reading my access token directly from localstorage. Upon logout the Authorization header by default becomes invalid and throws 401 now In your reponse interceptor you can redirect to logout page when refresh token fails. Hope this will help.

Axios Interceptor Response Token Refresh API called but getting Token is expired regardless in refreshToken API & lator all APIs

my axios interceptor is:-
axios.interceptors.response.use((response, error) => {
const originalRequest = response.config;
if (response.data.status === 'Token is Expired' && originalRequest.url === '/api/refresh') {
this.props.history.push('/logout');
Promise.reject(error);
}
if (response.data.status === 'Token is Expired' && !originalRequest._retry) {
originalRequest._retry = true;
const playerToken = localStorage.getItem('accessToken');
return axios
.get('/api/refresh', {
headers: {
Authorization: `bearer ${playerToken}`,
},
})
.then(res => {
console.log('from refreshtoken', res);
const stringRes = JSON.stringify(res);
const parsedRes = JSON.parse(stringRes);
const stringData = JSON.stringify(parsedRes.data);
const parsedData = JSON.parse(stringData);
const stringToken = JSON.stringify(parsedData.data);
const parsedToken = JSON.parse(stringToken);
if (parsedData.success == true) {
localStorage.setItem('accessToken', playerToken);
axios.response.config.headers['Authorization'] = `bearer ${parsedToken}`;
return Promise.resolve();
return axios(originalRequest);
} else {
this.props.history.push('/logout');
}
})
.catch(err => {
console.log('from refreshtoken', err);
});
}
return Promise.reject(error);
});
My code is running but when my refresh token API is called first time, It also returns the same status "Token is expired" due to which i am logged out of the app. This is happening only in interceptor. When i am calling Refresh API outside of interceptor, it returns with a refresh token.
Does my code have mistakes? or it is some other coding fault entirely.
Please answer & tell me the right way to do it & where do i place my interceptor??
Currently it is placed in a Component which is called just after login.
Usually the flow should be as such:
making a regular request with accessToken
request fails with status code 401
axios interceptor catches it and makes request to token/refresh. from that response it gets a new access token.
retries the original request.
So the code should looks like this (this is a working example from my app):
function isUnAuthorizedError(error) {
return error.config && error.response && error.response.status === 401;
}
function shouldRetry(config) {
return config.retries.count < 3;
}
function updateAuthToken(response) {
localStorage.setItem('token', response.data.accessToken);
}
async function authInterceptor(error) {
error.config.retries = error.config.retries || {
count: 0,
};
if (isUnAuthorizedError(error) && shouldRetry(error.config)) {
const response = await axios.post(`/token/refresh`, {});
updateAuthToken(response);
error.config.retries.count += 1;
axios.defaults.headers.common.Authorization = `Bearer ${response.data.accessToken}`; // update the accessToken
return axios.rawRequest(error.config); // retries the original request
}
return Promise.reject(error);
}
axios.interceptors.response.use(null, authInterceptor); // This indicates that authInterceptor will work only on request errors (status code >= 400)
Hopes this flow makes more sense.

Resources