Problem with purging persistor in 1 place but not another - react-native

Im trying to call persistor.purge() in my axios interceptor when my refresh token has expired, the idea is that I should log out. This is used on my log out button as well and it works there. But it rarely(only sometimes) purges through my interceptor function (I made sure to log before and after and the logs came through).
The onPress looks like this:
onPress={async () => {
await AsyncStorage.clear()
await persistor.purge()
}}
The interceptor looks like this:
apiInstance.interceptors.response.use(
(response) => {
return response
},
async (error) => {
const originalRequest = error.config
if (error.response.status === 401 && error.response.config.url === `api/token/refresh/`) {
await persistor.purge()
await AsyncStorage.clear()
return Promise.reject(error)
}
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
const refresh_token = await AsyncStorage.getItem('refresh')
const response = await apiInstance.post<{ access: string }>(`api/token/refresh/`, {
refresh: refresh_token,
})
const { access } = response.data
setAccessToken(access)
return apiInstance(originalRequest)
}
return Promise.reject(error)
}
)
What confuses me is that sometimes it works, sometimes it doesnt and sometimes I have to hot reload for the purge to call.
My guess is that the callbacks/promises are queued somehow or is called at the same time as an api request and just stops working. I have no idea... :(
Ive tried purging, flushing and pausing after eachother and theres no difference

Related

axios interceptor blocking api calls in redux saga

I have a react native project in which I'm calling some API's using redux-saga mechanism. Now when I added response interceptor for axios my saga api's are not working anymore. Does any knows how I can fix this?
here is the code for my axios instance class and response interceptor
const getLoggedInUser = async () => {
const savedUser = JSON.parse(
await getDataFromAsyncStorage(APP_CONSTANTS.SAVED_USER)
)
if (savedUser?.user_id != null) {
return savedUser
}
return null
}
const baseapi = axios.create({
baseURL: APP_CONSTANTS.BASE_URL,
headers: {},
})
baseapi.interceptors.request.use(
async (config) => {
const token = await getLoggedInUser()
const userId = token?.user_id
const authToken = token?.token
if (token) {
baseapi.defaults.headers.common['userId'] = token
baseapi.defaults.headers.common['token'] = authToken
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// Response interceptor for API calls
baseapi.interceptors.response.use(
(response) => {
return response
},
async function (error) {
const originalRequest = error.config
if (error.response.status === 403 /* && !originalRequest._retry */) {
return baseapi(originalRequest)
}
return Promise.reject(error)
}
)
This is my saga class code and it fails directly when I add a response interceptor
function* getTopicList(action) {
try {
yield put({type: ACTION_TYPES.START_TOPIC_LIST})
const {payload} = action
const res = yield call(getAllTopicsOfBatch, payload)
if (res?.status == APP_CONSTANTS.SUCCESS_STATUS) {
yield put({
type: ACTION_TYPES.SET_TOPIC_LIST,
payload: {data: res?.data?.topics},
})
} else {
alert('OOPS Something went wrong! Please try again')
yield put({
type: ACTION_TYPES.ERROR_TOPIC_LIST,
payload: 'Something Went Wrong Please Try Again',
})
}
} catch (error) {
console.log('RESPONES error', error)
alert('OOPS Something went wrong! Please try again')
yield put({
type: ACTION_TYPES.ERROR_TOPIC_LIST,
payload: 'Something Went Wrong Please Try Again',
})
}
}
The code looks mostly fine, the only two things I found that are likely causing problems are:
In the request interceptors you are likely wrongly passing the whole token as userId instead of userId
baseapi.defaults.headers.common['userId'] = token // 'token' should be 'userId'
In the response interceptors error handler, you are not guaranteed to have 'response' property on error.
if (error.response.status === 403) // use error?.response
If neither of these things will fix your problem my guess is you have a problem in your endpoint and so you should examine the response errors you get to guide you.

react-native-community/asyncStorage removeItem causes program to behave weirdly

I have this little code snippet executed during the user logout.
async function logoutAction(props) {
removeUser();
props.logoutUser();
}
The function inside removeUser() is as :
export const removeUser = async () => {
try {
await AsyncStorage.removeItem(Constant.storage.user_data);
await AsyncStorage.removeItem(Constant.storage.token);
await AsyncStorage.removeItem(Constant.storage.notification_token);
return true;
} catch (exception) {
return false;
}
}
This clears user related data from local storage.
Similarly, props.logoutUser() is a reference call to reducer which sets loggedIn status to false.
I'm having this issue that if the removeUser() function is called once, the axios http requests do not enter the interceptors anymore and every request catches an error 'undefined'. If this method is removed though, everything works fine.
I can get it to working state then by removing the interceptors once, performing a request and then adding the interceptors again, which I found after hours of here and there.
My interceptors are:
export const requestInterceptor = axios.interceptors.request.use(
async config => {
const token = await getToken();
if (token != '') {
config.headers.Authorization = token;
}
console.log('axios request', config);
return config;
},
error => {
// console.warn('on request error')
return Promise.reject(error);
},
);
export const responseInterceptor = axios.interceptors.response.use(
function(response) {
console.log('axios response', response);
// console.warn('on response success', response.status)
return response;
},
async function(error) {
if (error.response.status === 401) {
//logout user
return;
}
return Promise.reject(error);
},
);
I am using the #react-native-community/AsyncStorage package for maintaining local storage. I suspect that the issue might be in the removeItem method but I'm unsure as the official docs don't contain the removeItem method, or in the interceptor which doesn't seem faulty to me anyways.
What am I doing wrong here?? Please show me some light..
Or maybe try add a await before removeUser(); ?
async function logoutAction(props) {
await removeUser();
props.logoutUser();
}
The issue was quite silly and did not even concern AsyncStorage or removeItem and as Matt Aft pointed out in the comment, it was due to the call for token in the interceptor after it had been removed while logging out. So, replacing
const token = await getToken();
if (token != '') {
config.headers.Authorization = token;
}
by
await getToken()
.then(token => {
config.headers.Authorization = token;
})
.catch(_ => {
console.log('no token');
});
in the interceptor and returning promise from the getToken method did the thing.
Thanks to Matt and 高鵬翔.

How can I remove the `GET http://localhost:5000/api/users/profile 401 (Unauthorized)` showing in the console?

I am setting up refresh and access token system for my Vue web application. http://localhost:5000/api/users/profile is the URL of my POST request. I expect an error when someone tries to access and their access token has expired. I use interceptors from Axios in order to generate a brand new access token when such error appears. Everything works fine. However, I've spent a lot of time trying to figure out how to get rid of the GET http://localhost:5000/api/users/profile 401 (Unauthorized) in console. Is there any way to rid of it? Any help would be appreciated.
Getting profile:
getProfile: async (context) => {
context.commit('PROFILE_REQUEST')
let res = await axios.get('http://localhost:5000/api/users/profile')
context.commit('USER_PROFILE', res.data.user);
return res;
}
Interceptor:
axios.interceptors.response.use((res) => {
return res;
}, async (err) => {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = Cookies.get('jid');
return axios.post('http://localhost:5000/api/users/refresh-token', { refreshToken }).then((res) => {
axios.defaults.headers.common['Authorization'] = `Bearer ${res.data.accessToken}`;
originalRequest.headers['Authorization'] = `Bearer ${res.data.accessToken}`;
originalRequest.baseURL = undefined;
return axios(originalRequest);
})
}
return Promise.reject(err);
});
}

response of axios retry in main.js in vue js

I have a method named getUsers and it is in created hook in Users Component and I have access token and refresh token in my local storage.
I want that when my token expires, I use refresh token and get new access token and retry last request that was failed because of expired access token.
My problem is I want get response of second try of axios call in first axios call point (in Users component in created hook) because I fill table from response of it.
How can I do that?
main.js:
axios.interceptors.request.use((config) => {
config.headers['Content-Type'] = `application/json`;
config.headers['Accept'] = `application/json`;
config.headers['Authorization'] = `Bearer ${localStorage.getItem('access_token')}`;
return config;
}, (err) => {
return Promise.reject(err);
});
let getRefreshError = false
axios.interceptors.response.use((response) => {
return response
},
(error) => {
const originalRequest = error.config;
if (!getRefreshError && error.response.status === 401) {
axios.post(process.env.VUE_APP_BASE_URL + process.env.VUE_APP_REFRESH_TOKEN,
{refresh_token: localStorage.getItem("refresh_token")})
.then(res => {
localStorage.setItem("access_token", res.data.result.access_token);
localStorage.setItem("refresh_token", res.data.result.refresh_token);
originalRequest.headers['Authorization'] = localStorage.getItem("access_token");
return axios(originalRequest)
.then((res) => {
return Promise.resolve(res);
}, (err) => {
return Promise.reject(err);
});
}).catch(error => {
getRefreshError = true;
router.push('/pages/login')
return Promise.reject(error);
})
}
return Promise.reject(error);
});
Users:
created() {
this.getUsers();
}
You can return a new Promise from error handler of response interceptor. Refresh token there, perform the original request and resolve promise based on the result of actions (refreshing and re-fetching). Here is a general sketch of what you should do.
axios.interceptors.response.use(
(res => res),
(err => {
return new Promise(resolve, reject) => {
// refresh token
// then save the token
// then reperform original request
// and resolve with the response of the original request.
resolve(resOfSecondRequest)
// in case of any error, reject with the error
// and catch it where original call was performed just like the normal flow
reject(errOfSecondRequest)
}
})
)

How can I get an axios interceptor to retry the original request?

I am trying to implement a token refresh into my vue.js application. This is working so far, as it refreshes the token in the store on a 401 response, but all I need to do is get the interceptor to retry the original request again afterwards.
main.js
axios.interceptors.response.use(
response => {
return response;
},
error => {
console.log("original request", error.config);
if (error.response.status === 401 && error.response.statusText === "Unauthorized") {
store.dispatch("authRefresh")
.then(res => {
//retry original request???
})
.catch(err => {
//take user to login page
this.router.push("/");
});
}
}
);
store.js
authRefresh(context) {
return new Promise((resolve, reject) => {
axios.get("auth/refresh", context.getters.getHeaders)
.then(response => {
//set new token in state and storage
context.commit("addNewToken", response.data.data);
resolve(response);
})
.catch(error => {
reject(error);
});
});
},
I can log the error.config in the console and see the original request, but does anyone have any idea what I do from here to retry the original request? and also stop it from looping over and over if it fails.
Or am I doing this completely wrong? Constructive criticism welcome.
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);
});
Implementation proposed by #Patel Pratik is good but only handles one request at a time.
For multiple requests, you can simply use axios-auth-refresh package. As stated in documentation:
The plugin stalls additional requests that have come in while waiting
for a new authorization token and resolves them when a new token is
available.
https://www.npmjs.com/package/axios-auth-refresh
#Patel Pratik, thank you.
In react native, I've used async storage and had custom http header, server needed COLLECTORACCESSTOKEN, exactly in that format (don't say why =)
Yes, I know, that it shoud be secure storage.
instance.interceptors.response.use(response => response,
async error => { -----it has to be async
const originalRequest = error.config;
const status = error.response?.status;
if (status === 401 && !originalRequest.isRetry) {
originalRequest.isRetry = true;
try {
const token = await AsyncStorage.getItem('#refresh_token')
const res = await axios.get(`${BASE_URL}/tokens/refresh/${token}`)
storeAccess_token(res.data.access_token)
storeRefresh_token(res.data.refresh_token)
axios.defaults.headers.common['COLLECTORACCESSTOKEN'] =
res.data.access_token;
originalRequest.headers['COLLECTORACCESSTOKEN'] =
res.data.access_token;
return axios(originalRequest);
} catch (e) {
console.log('refreshToken request - error', e)
}
}
if (error.response.status === 503) return
return Promise.reject(error.response.data);
});
Building on #Patel Praik's answer to accommodate multiple requests running at the same time without adding a package:
Sorry I don't know Vue, I use React, but hopefully you can translate the logic over.
What I have done is created a state variable that tracks whether the process of refreshing the token is already in progress. If new requests are made from the client while the token is still refreshing, I keep them in a sleep loop until the new tokens have been received (or getting new tokens failed). Once received break the sleep loop for those requests and retry the original request with the updated tokens:
const refreshingTokens = useRef(false) // variable to track if new tokens have already been requested
const sleep = ms => new Promise(r => setTimeout(r, ms));
axios.interceptors.response.use(function (response) {
return response;
}, async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// if the app is not already requesting a new token, request new token
// i.e This is the path that the first request that receives status 401 takes
if (!refreshingTokens.current) {
refreshingTokens.current = true //update tracking state to say we are fething new tokens
const refreshToken = localStorage.getItem('refresh_token')
try {
const newTokens = await anAxiosInstanceWithoutInterceptor.post(`${process.env.REACT_APP_API_URL}/user/token-refresh/`, {"refresh": refreshToken});
localStorage.setItem('access_token', newTokens.data.access);
localStorage.setItem('refresh_token', newTokens.data.refresh);
axios.defaults.headers['Authorization'] = "JWT " + newTokens.data.access
originalRequest.headers['Authorization'] = "JWT " + newTokens.data.access
refreshingTokens.current = false //update tracking state to say new
return axios(originalRequest)
} catch (e) {
await deleteTokens()
setLoggedIn(false)
}
refreshingTokens.current = false //update tracking state to say new tokens request has finished
// if the app is already requesting a new token
// i.e This is the path the remaining requests which were made at the same time as the first take
} else {
// while we are still waiting for the token request to finish, sleep for half a second
while (refreshingTokens.current === true) {
console.log('sleeping')
await sleep(500);
}
originalRequest.headers['Authorization'] = "JWT " +
localStorage.getItem('access_token');
return axios(originalRequest)
}
}
return Promise.reject(error);
});
If you don't want to use a while loop, alternatively you could push any multiple request configs to a state variable array and add an event listener for when the new tokens process is finished, then retry all of the stored arrays.