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 高鵬翔.
Related
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
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.
Login routes:
router.post("/login", async (req, res) => {
try {
const user = await User.findOne({
mobileNo: req.body.mobileNo,
});
if (!user) {
res.status(401).json("You are not registerd");
}
const password = res.body.password;
if (password === user.password) {
return res.status(200).json("You are logged in");
} else {
return res.status(501).json("Naah! wrong pass");
}
} catch {
(err) => {
res.status(500).json(err);
};
}
});
module.exports = router;
index.js:
app.use("/api/auth", authRoute);
import:
const authRoute = require("./routes/auth");
My postman image, I am not getting any result.
Your try-catch syntax is wrong, correct would be
try {
...
} catch(err) {
res.status(500).json(err);
}
With your syntax, when the catch block is reached, the res.status(500).json(err) statement is not executed, therefore the request never comes back.
In your try block, there are 3 responses available. If the first condition in the if block is also executed with another one of the responses in the below if-else condition this problem may occur. Because at a time, sending 2 responses is impossible. Therefore, you should return that response and terminate.
if (!user) {
return res.status(401).json("You are not registered");
}
This is part 2 of me debugging my application in production
In part 1, I managed to at least see what was causing my problem and managed to solve that.
When I send a request to my API which is hosted on Heroku using axios interceptor, every single request object looks like this in the API
{ 'object Object': '' }
Before sending out data to the API, I console.log() the transformRequest in axios and I can see that the data I am sending is actually there.
Note: I have tested this process simply using
axios.<HTTP_METHOD>('my/path', myData)
// ACTUAL EXAMPLE
await axios.post(
`${process.env.VUE_APP_BASE_URL}/auth/login`,
userToLogin
);
and everything works and I get data back from the server.
While that is great and all, I would like to abstract my request implementation into a separate class like I did below.
Does anyone know why the interceptor is causing this issue? Am I misusing it?
request.ts
import axios from "axios";
import { Message } from "element-ui";
import logger from "#/plugins/logger";
import { UsersModule } from "#/store/modules/users";
const DEBUG = process.env.NODE_ENV === "development";
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_URL,
timeout: 5000,
transformRequest: [function (data) {
console.log('data', data)
return data;
}],
});
service.interceptors.request.use(
config => {
if (DEBUG) {
logger.request({
method: config.method,
url: config.url
});
}
return config;
},
error => {
return Promise.reject(error);
}
);
service.interceptors.response.use(
response => {
console.log('axios interception response', response)
return response.data;
},
error => {
const { response } = error;
console.error('axios interception error', error)
if (DEBUG) {
logger.error(response.data.message, response);
}
Message({
message: `Error: ${response.data.message}`,
type: "error",
duration: 5 * 1000
});
return Promise.reject({ ...error });
}
);
export default service;
Login.vue
/**
* Sign user in
*/
async onClickLogin() {
const userToLogin = {
username: this.loginForm.username,
password: this.loginForm.password
};
try {
const res = await UsersModule.LOGIN_USER(userToLogin);
console.log("res", res);
this.onClickLoginSuccess();
} catch (error) {
throw new Error(error);
}
}
UsersModule (VUEX Store)
#Action({ rawError: true })
async [LOGIN_USER](params: UserSubmitLogin) {
const response: any = await login(params);
console.log('response in VUEX', response)
if (typeof response !== "undefined") {
const { accessToken, username, name, uid } = response;
setToken(accessToken);
this.SET_UID(uid);
this.SET_TOKEN(accessToken);
this.SET_USERNAME(username);
this.SET_NAME(name);
}
}
users api class
export const login = async (data: UserSubmitLogin) => {
return await request({
url: "/auth/login",
method: "post",
data
});
};
I'm not sure what you're trying to do with transformRequest but that probably isn't what you want.
A quote from the documentation, https://github.com/axios/axios#request-config:
The last function in the array must return a string or an instance of Buffer, ArrayBuffer, FormData or Stream
If you just return a normal JavaScript object instead it will be mangled in the way you've observed.
transformRequest is responsible for taking the data value and converting it into something that can actually be sent over the wire. The default implementation does quite a lot of work manipulating the data and setting relevant headers, in particular Content-Type. See:
https://github.com/axios/axios/blob/885ada6d9b87801a57fe1d19f57304c315703079/lib/defaults.js#L31
If you specify your own transformRequest then you are replacing that default, so none of that stuff will happen automatically.
Without knowing what you're trying to do it's difficult to advise further but you should probably use a request interceptor rather than transformRequest for whatever it is you're trying to do.
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.