How do I cancel all pending Axios requests in React Native? - 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]);

Related

How to cancel axios request?

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
)

RN "TypeError: Network request failed" - production - random

I know some questions about the subject has been opened here and there, but my issue is different :
all the other ones appear in dev mode, in my case it's in production,
a very big percentage of requests pass, a few of them is TypeError: Network request failed - but sometimes for critical requests
it's random, not always the same request. Sometimes it passes, sometimes not.
it appears to three on my projects, one is on AWS the other one on Clever-Cloud, both are projects between 1000 and 5000 users, servers are quite too big for what they do - I think I removed the risk of a server fault. Even if... I can reproduce locally when I don't start the api locally. So it's like the api is not responding, but as I said, I don't think so.
I have no clue where to dig anymore...
I can give you my API.js service file, maybe you'll find what's wrong ?
import URI from 'urijs';
import { Platform } from 'react-native';
import NetInfo from '#react-native-community/netinfo';
import { getUserToken, wipeData } from '../utils/data';
import { SCHEME, MW_API_HOST } from '../config';
import deviceInfoModule from 'react-native-device-info';
import { capture } from '../utils/sentry';
const unauthorisedHandler = (navigation) => {
wipeData();
navigation.reset({ index: 0, routes: [{ name: 'Auth' }] });
};
const checkNetwork = async (test = false) => {
const isConnected = await NetInfo.fetch().then((state) => state.isConnected);
if (!isConnected || test) {
await new Promise((res) => setTimeout(res, 1500));
return false;
}
return true;
};
class ApiService {
host = MW_API_HOST;
scheme = SCHEME;
getUrl = (path, query) => {
return new URI().host(this.host).scheme(this.scheme).path(path).setSearch(query).toString();
};
execute = async ({ method = 'GET', path = '', query = {}, headers = {}, body = null }) => {
try {
const config = {
method,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
appversion: deviceInfoModule.getBuildNumber(),
appdevice: Platform.OS,
currentroute: this.navigation?.getCurrentRoute?.()?.name,
...headers,
},
body: body ? JSON.stringify(body) : null,
};
const url = this.getUrl(path, query);
console.log('url: ', url);
const canFetch = await checkNetwork();
if (!canFetch) return;
let response;
// To try to avoid mysterious `TypeError: Network request failed` error
// that throws an error directly
// we try catch and try one more time.
try {
response = await fetch(url, config);
} catch (e) {
if (e?.toString().includes('Network request failed')) {
// try again
await new Promise((res) => setTimeout(res, 250));
console.log('try again because Network request failed');
response = await fetch(url, config);
} else {
throw e;
}
}
if (!response.ok) {
if (response.status === 401) {
const token = await getUserToken();
if (token) unauthorisedHandler(API.navigation);
return response;
}
}
if (response.json) return await response.json();
return response;
} catch (e) {
capture(e, { extra: { method, path, query, headers, body } });
return { ok: false, error: "Sorry, an error occured, technical team has been warned." };
}
};
executeWithToken = async ({ method = 'GET', path = '', query = {}, headers = {}, body = null }) => {
const token = await getUserToken();
if (token) headers.Authorization = token;
return this.execute({ method, path, query, headers, body });
};
get = async (args) => this.executeWithToken({ method: 'GET', ...args });
post = async (args) => this.executeWithToken({ method: 'POST', ...args });
put = async (args) => this.executeWithToken({ method: 'PUT', ...args });
delete = async (args) => this.executeWithToken({ method: 'DELETE', ...args });
}
const API = new ApiService();
export default API;
Talking with experts here and there, it seems that it's normal : internet network is not 100% reliable, so sometimes, request fail, for a reason that we can't anticipate (tunnel, whatever).
I ended up using fetch-retry and I still have a few of those, but much less !

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;
});

Axios interceptors don't send data to API in production Heroku app

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.

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.