How to cancel axios request? - react-native

I have TextInput and I need to send request every time when the text is changing
I have this code:
// Main.js
import Api from 'network/ApiManager';
const api = new Api();
// TextInput onChangeText function
const getSearch = useCallback(
async (searchName, sectorTypeId, type, filterData) => {
const result = await api.controller.search(searchName, sectorTypeId, type, filterData);
console.log(result)
},
[],
);
And i have this network layer
// NetworkManager.js
async getData(url) {
try {
const {data: response} = await axios.get(url);
return response;
} catch (e) {
return response;
}
}
controller = {
profile: async (search, sector, f_type, filterData = {}) => {
const res = await this.getData('/url/path');
return this.transformToOptions(res);
},
};
When onChangeText is called, I send a lot of requests, but I want to cancel previous requests and get the latest only. I know that I need to use CancelToken but I don't know how to pass it on my network layer
Please help

You can create a cancelToken, whenever a request comes, you can save the cancel token, when a new request comes, cancelToken won't be undefined, thus you can call cancelToken.cancel(). Try something like this:
let cancelToken
if (typeof cancelToken != typeof undefined) {
cancelToken.cancel("Operation canceled due to new request.")
}
//Save the cancel token for the current request
cancelToken = axios.CancelToken.source()
try {
const results = await axios.get(
`Your URL here`,
{ cancelToken: cancelToken.token } //Pass the cancel token
)

Related

How do I cancel all pending Axios requests in React Native?

My React Native app uses axios to connect to a backend. This is done in the file myApi.js:
class client {
axiosClient = axios.create({
baseURL: example.com,
});
async get(url, data, config) {
return this.axiosClient.get(url, data, config);
}
async put(url, data, config) {
return this.axiosClient.put(url, data, config);
}
async post(url, data, config) {
return this.axiosClient.post(url, data, config);
}
}
export default new client();
I have a component which contains a useEffect which is controlled by a date picker. The selected date is held in a context called DateContext. When the selected date changes, a request is fired off to get some data for that date. When data is returned, it is displayed to the user.
The Component is:
const DaySelect = () => {
const {dateState} = useContext(DateContext);
useEffect(() => {
const load = () => {
const url = '/getDataForDate';
const req = {
selectedDate: moment(dateState.currentDate).format(
'YYYY-MM-DD',
),
};
myApi
.post(url, req)
.then((res) => {
// Now put results into state so they will be displayed
})
};
load();
}, [dateState.currentDate]);
return (
<View>
<>
<Text>Please select a date.</Text>
<DateSelector />
<Results />
</>
)}
</View>
);
};
export default DaySelect;
DateSelector is just the Component where the date is selected; any change to the date updates the value of dateState.currentDate. Results displays the data.
This works fine as long as the user clicks a date, and waits for the results to show before clicking a new date. However, if they click several times, then a request is fired off each time and the resulting data is displayed as each request completes. Since the requests don't finish in the order that they start, this often results in the wrong data being shown.
To avoid this, I believe I need to cancel any existing requests before making a new request. I've tried to do this using Abort Controller, but it doesn't seem to do anything.
I added the following to myApi.js:
const controller = new AbortController();
class client {
axiosClient = axios.create({
baseURL: example.com,
});
async get(url, data, config) {
return this.axiosClient.get(url, data, config);
}
async put(url, data, config) {
return this.axiosClient.put(url, data, config);
}
async post(url, data, config) {
return this.axiosClient.post(url, data, config, {signal: controller.signal});
}
async cancel() {
controller.abort();
}
}
export default new client();
Then in my main component I do
myApi.cancel()
before making the new request.
This doesn't seem to do anything, though - the requests don't get cancelled. What am I doing wrong here?
EDIT: Following Ibrahim's suggestion below, I changed the api file to:
const cancelTokenSource = axios.CancelToken.source();
class client {
axiosClient = axios.create({
baseURL: example.com,
});
async post(url, data, config) {
const newConfig = {
...config,
cancelToken: cancelTokenSource.token
};
return this.axiosClient.post(url, data, newConfig);
}
async cancel() { // Tried this with and without async
cancelTokenSource.cancel();
}
}
export default new client();
This makes the api call fail entirely.
Step1: Generate cancel token
const cancelTokenSource = axios.CancelToken.source();
Step2: Assign cancel token to each request
axios.get('example.com/api/getDataForDate', {
cancelToken: cancelTokenSource.token
});
// Or if you are using POST request
axios.post('example.com/api/postApi', {data}, {
cancelToken: ancelTokenSource.token,
});
Step3: Cancel request using cancel token
cancelTokenSource.cancel();
In myApi I used AbortController to ensure that any cancellable requests are aborted when a new cancellable request comes in:
let controller = new AbortController();
class client {
axiosClient = axios.create({
baseURL: example.com,
});
async post(url, data, config, stoppable) {
let newConfig = {...config};
// If this call can be cancelled, cancel any existing ones
// and set up a new AbortController
if (stoppable) {
if (controller) {
controller.abort();
}
// Add AbortSignal to the request config
controller = new AbortController();
newConfig = {...newConfig, signal: controller.signal};
}
return this.axiosClient.post(url, data, newConfig);
}
}
export default new client();
Then in my component I pass in 'stoppable' as true; after the call I check whether the call was aborted or not. If not, I show the results; otherwise I ignore the response:
useEffect(() => {
const load = () => {
const url = '/getDataForDate';
const req = {
selectedDate: moment(dateState.currentDate).format(
'YYYY-MM-DD',
),
};
myApi
.post(url, req, null, true)
.then((res) => {
if (!res.config.signal.aborted) {
// Do something with the results
}
})
.catch((err) => {
// Show an error if the request has failed entirely
});
};
load();
}, [dateState.currentDate]);

In reactnative expo I tried using secureStore from expo in redux to save token the one I get from api

I tried using redux to save token the one I get from api in react native ..its working now.
First one is for settoken and other one is for gettoken.
enter image description here
export const verifyOTP = (formValues, actions) => {
return async (dispatch) => {
dispatch(startSubmitting());
const url = `/validate-otp`;
var formdata = new FormData();
formdata.append("mobile", formValues.mobile);
formdata.append("otp", formValues.otp);
const response = await api.post(url, formdata);
dispatch({
type: "VERIFY_OTP",
payload: response,
});
dispatch(stopSubmitting());
await SecureStore.setItemAsync("userToken", response.data.access_token);
};
};
export const checkUser = () => {
return async (dispatch) => {
const token = await SecureStore.getItemAsync("userToken");
const url = `/me`;
const response = await api
.post(url, { token })
.then((res) => {
return res;
})
.catch((error) => {
return error.response;
});
dispatch({
type: "CHECK_USER",
payload: response,
});
};
};
The Problem
you are mixing two different implementations in checkUser to handle a promise which is clearly incorrect and leads to the issues.
The Solution
since your other parts of codes use the async/await so try to remove then/catch block from the response constant:
const checkUser = () => {
return async (dispatch) => {
const url = '/me';
try {
const token = await SecureStore.getItemAsycn("userToken);
const response = await api.post(url, {token})
dispatch({type: "CHECK_USER", payload: response})
} catch (error) {
// to proper action on failure case
}
}
}
Note 1: always use async/await in try/catch block. more on MDN documentation.
Optional
since you are trying to call two async actions (once for getting token and once for calling '/me' API), I encourage you to use two different try/catch blocks to handle the failure case for each async action separately. for example:
const checkUser = () => {
return async (dispatch) => {
let token = null;
try {
token = await SecureStore.getItemAsync("userToken");
} catch (err) {
// proper action in case of failure on getting the token from storage
}
// you may need to ignore API calls without the token, so:
try {
if(token){
const url = '/me';
const response = await api.post(url, {token});
dispatch({type: "CHECK_USER", payload: response});
}
} catch (err) {
// take proper action with the error response according to your applicaiton
}
}
}

Nextjs Auth0 get data in getServerSideProps

Im using Auth0 to authenticate users.
Im protected api routes like this:
// pages/api/secret.js
import { withApiAuthRequired, getSession } from '#auth0/nextjs-auth0';
export default withApiAuthRequired(function ProtectedRoute(req, res) {
const session = getSession(req, res);
const data = { test: 'test' };
res.json({ data });
});
My problem is when I'm trying to fetch the data from getServerSideProps I'm getting 401 error code.
If I use useEffect Im able to get data from api route.
Im trying to fetch the data like this:
export const getServerSideProps = withPageAuthRequired({
async getServerSideProps(ctx) {
const res = await fetch('http://localhost:3000/api/secret');
const data = await res.json();
return { props: { data } };
},
});
Im getting the following response:
error: "not_authenticated", description: "The user does not have an active session or is not authenticated"
Any idea guys? Thanks!!
When you call from getServerSideProps the protected API end-point you are not passing any user's context (such as Cookies) to the request, therefore, you are not authenticated.
When you call from useEffect it runs inside your browser, which attaches all cookies to the request, one of them is the session cookie.
You need to forward the session cookie that was passed to the getServerSideProps (by the browser) to the API call.
export const getServerSideProps = withPageAuthRequired({
async getServerSideProps(ctx) {
const res = await fetch('http://localhost:3000/api/secret', {
headers: { Cookie: ctx.req.headers.cookie },
// ---------------------------^ this req is the browser request to the getServersideProps
});
const data = await res.json();
return { props: { data } };
},
});
For more info.
#auth0/nextjs-auth0 has useUser hook. This example is from: https://auth0.com/blog/ultimate-guide-nextjs-authentication-auth0/
// pages/index.js
import { useUser } from '#auth0/nextjs-auth0';
export default () => {
const { user, error, isLoading } = useUser();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>{error.message}</div>;
if (user) {
return (
<div>
Welcome {user.name}! Logout
</div>
);
}
// if not user
return Login;
};
Note that authentication takes place on the server in this model,
meaning that the client isn't aware that the user is logged in. The
useUser hook makes it aware by accessing that information in the
initial state or through the /api/auth/profile endpoint, but it won't
expose any id_token or access_token to the client. That information
remains on the server side.
Custom HOF:
// getData is a callback function
export const withAuth = (getData) => async ({req, res}) => {
const session = await auth0.getSession(req);
if (!session || !session.user) {
res.writeHead(302, {
Location: '/api/v1/login'
});
res.end();
return {props: {}};
}
const data = getData ? await getData({req, res}, session.user) : {};
return {props: {user: session.user, ...data}}
}
Example of using:
export const getServerSideProps = withAuth(async ({req, res}, user) => {
const title = await getTitle();
return title;
});

React native fetch from URL every X seconds

In the page I have two things to do, first I fetch some content from API and display it. After that, I will fetch another API every 5 seconds to see whether the status of the content displayed has been changed or not.
In my MyScreen.js
const MyScreen = props => {
const dispatch = useDispatch();
const onConfirmHandler = async () => {
try {
//get content from API and display on the screen
const response1 = await dispatch(action1(param1,param2));
if(response1==='success'){
// I want to check the result of response2 every 5 seconds, how can I do this?
const response2 = await dispatch(action2(param3));
}
}catch {...}
}
return (
<Button title='confirm' onPress={onConfirmHandler}}>
)
}
The actions I fetch the API in actions.js:
export default action1 = (param1,param2) ={
return async (dispatch, getState) => {
// To call the API, I need to used token I got when login
let = getState().login.token;
const response = await fetch(url,body);
const resData = await response.json();
if (resData==='success'){
return param3
}
}
export default action2 = (param3) ={
return async (dispatch, getState) => {
// To call the API, I need to used token I got when login
let = getState().login.token;
const response = await fetch(url,body);
const resData = await response.json();
if (resData==='success'){
// if the content status changed, I change the view in MyScreen.js
return 'changed';
}
}
I have also met this problem already. Here you can't use the timeout function. Instead, you can use react-native-socketio or react-native-background-fetch packages. I prefer react-native-background-fetch. So you can fetch results at a specific interval and update the state on change of content.

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.