Abort Axios request if function is called again - javascript

I'm trying to find a way to cancel axios requests if another request happens before the first request is complete.
I want it to work as a function. I have made a function looking like this:
import axios from "axios";
async function paginatedActivities(pageNumber) {
let controller = new AbortController();
try {
const fetchData = await axios.get(
`${process.env.SERVERBASEURL}/activities/paginatedactivities`,
{
signal: controller.signal,
params: { query: "query", pageNumber: pageNumber },
withCredentials: true,
}
);
return fetchData.data;
} catch (err) {
console.log(err);
if (axios.isCancel(err)) console.log("request cancelled");
}
}
export default paginatedActivities;
I can't figure out when to call "controller.abort()"..
The function could potentially be called again before it has done its job, and in such cases i'd need it to abort.. any suggestions? (without using the "useEffect"-hook..)

Make the controller a global variable so you can access it between calls of the function
let controller;
async function paginatedActivities(pageNumber) {
if (controller) controller.abort();
controller = new AbortController();
// Make the request
controller = undefined; // Reset the variable so it's not aborted next time
}

Related

Avoid multiple time api calls when user clicks on button accidently multiple times in javascript/React.js [duplicate]

I use axios for ajax requests and reactJS + flux for render UI. In my app there is third side timeline (reactJS component). Timeline can be managed by mouse's scroll. App sends ajax request for the actual data after any scroll event. Problem that processing of request at server can be more slow than next scroll event. In this case app can have several (2-3 usually) requests that already is deprecated because user scrolls further. it is a problem because every time at receiving of new data timeline begins redraw. (Because it's reactJS + flux) Because of this, the user sees the movement of the timeline back and forth several times. The easiest way to solve this problem, it just abort previous ajax request as in jQuery. For example:
$(document).ready(
var xhr;
var fn = function(){
if(xhr && xhr.readyState != 4){
xhr.abort();
}
xhr = $.ajax({
url: 'ajax/progress.ftl',
success: function(data) {
//do something
}
});
};
var interval = setInterval(fn, 500);
);
How to cancel/abort requests in axios?
Axios does not support canceling requests at the moment. Please see this issue for details.
UPDATE: Cancellation support was added in axios v0.15.
EDIT: The axios cancel token API is based on the withdrawn cancelable promises proposal.
UPDATE 2022: Starting from v0.22.0 Axios supports AbortController to cancel requests in fetch API way:
Example:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
Using useEffect hook:
useEffect(() => {
const ourRequest = Axios.CancelToken.source() // <-- 1st step
const fetchPost = async () => {
try {
const response = await Axios.get(`endpointURL`, {
cancelToken: ourRequest.token, // <-- 2nd step
})
console.log(response.data)
setPost(response.data)
setIsLoading(false)
} catch (err) {
console.log('There was a problem or request was cancelled.')
}
}
fetchPost()
return () => {
ourRequest.cancel() // <-- 3rd step
}
}, [])
Note: For POST request, pass cancelToken as 3rd argument
Axios.post(`endpointURL`, {data}, {
cancelToken: ourRequest.token, // 2nd step
})
Typically you want to cancel the previous ajax request and ignore it's coming response, only when a new ajax request of that instance is started, for this purpose, do the following:
Example: getting some comments from API:
// declare an ajax request's cancelToken (globally)
let ajaxRequest = null;
function getComments() {
// cancel previous ajax if exists
if (ajaxRequest ) {
ajaxRequest.cancel();
}
// creates a new token for upcomming ajax (overwrite the previous one)
ajaxRequest = axios.CancelToken.source();
return axios.get('/api/get-comments', { cancelToken: ajaxRequest.token }).then((response) => {
console.log(response.data)
}).catch(function(err) {
if (axios.isCancel(err)) {
console.log('Previous request canceled, new request is send', err.message);
} else {
// handle error
}
});
}
import React, { Component } from "react";
import axios from "axios";
const CancelToken = axios.CancelToken;
let cancel;
class Abc extends Component {
componentDidMount() {
this.Api();
}
Api() {
// Cancel previous request
if (cancel !== undefined) {
cancel();
}
axios.post(URL, reqBody, {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
}),
})
.then((response) => {
//responce Body
})
.catch((error) => {
if (axios.isCancel(error)) {
console.log("post Request canceled");
}
});
}
render() {
return <h2>cancel Axios Request</h2>;
}
}
export default Abc;
There is really nice package with few examples of usage called axios-cancel.
I've found it very helpful.
Here is the link: https://www.npmjs.com/package/axios-cancel
https://github.com/axios/axios#cancellation
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
let url = 'www.url.com'
axios.get(url, {
progress: false,
cancelToken: source.token
})
.then(resp => {
alert('done')
})
setTimeout(() => {
source.cancel('Operation canceled by the user.');
},'1000')
This is how I did it using promises in node. Pollings stop after making the first request.
var axios = require('axios');
var CancelToken = axios.CancelToken;
var cancel;
axios.get('www.url.com',
{
cancelToken: new CancelToken(
function executor(c) {
cancel = c;
})
}
).then((response) =>{
cancel();
})
Using cp-axios wrapper you able to abort your requests with three diffent types of the cancellation API:
1. Promise cancallation API (CPromise):
Live browser example
const cpAxios= require('cp-axios');
const url= 'https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=5s';
const chain = cpAxios(url)
.timeout(5000)
.then(response=> {
console.log(`Done: ${JSON.stringify(response.data)}`)
}, err => {
console.warn(`Request failed: ${err}`)
});
setTimeout(() => {
chain.cancel();
}, 500);
2. Using AbortController signal API:
const cpAxios= require('cp-axios');
const CPromise= require('c-promise2');
const url= 'https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=5s';
const abortController = new CPromise.AbortController();
const {signal} = abortController;
const chain = cpAxios(url, {signal})
.timeout(5000)
.then(response=> {
console.log(`Done: ${JSON.stringify(response.data)}`)
}, err => {
console.warn(`Request failed: ${err}`)
});
setTimeout(() => {
abortController.abort();
}, 500);
3. Using a plain axios cancelToken:
const cpAxios= require('cp-axios');
const url= 'https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=5s';
const source = cpAxios.CancelToken.source();
cpAxios(url, {cancelToken: source.token})
.timeout(5000)
.then(response=> {
console.log(`Done: ${JSON.stringify(response.data)}`)
}, err => {
console.warn(`Request failed: ${err}`)
});
setTimeout(() => {
source.cancel();
}, 500);
4. Usage in a custom React hook (Live Demo):
import React from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpAxios from "cp-axios";
/*
Note: the related network request will be aborted as well
Check out your network console
*/
function TestComponent({ url, timeout }) {
const [cancel, done, result, err] = useAsyncEffect(
function* () {
return (yield cpAxios(url).timeout(timeout)).data;
},
{ states: true, deps: [url] }
);
return (
<div>
{done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
<button onClick={cancel} disabled={done}>
Cancel async effect (abort request)
</button>
</div>
);
}
Update
Axios v0.22.0+ supports AbortController natively:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
Starting from v0.22.0 Axios supports AbortController to cancel requests in fetch API way:
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// cancel the request
controller.abort()
CancelToken deprecated
You can also cancel a request using a CancelToken.
The axios cancel token API is based on the withdrawn cancelable promises proposal.
This API is deprecated since v0.22.0 and shouldn't be used in new projects
You can create a cancel token using the CancelToken.source factory as shown below:
import {useState, useEffect} from 'react'
export function useProfileInformation({accessToken}) {
const [profileInfo, setProfileInfo] = useState(null)
useEffect(() => {
const abortController = new AbortController()
window
.fetch('https://api.example.com/v1/me', {
headers: {Authorization: `Bearer ${accessToken}`},
method: 'GET',
mode: 'cors',
signal: abortController.signal,
})
.then(res => res.json())
.then(res => setProfileInfo(res.profileInfo))
return function cancel() {
abortController.abort()
}
}, [accessToken])
return profileInfo
}
// src/app.jsx
import React from 'react'
import {useProfileInformation} from './hooks/useProfileInformation'
export function App({accessToken}) {
try {
const profileInfo = useProfileInformation({accessToken})
if (profileInfo) {
return <h1>Hey, ${profileInfo.name}!</h1>
} else {
return <h1>Loading Profile Information</h1>
}
} catch (err) {
return <h1>Failed to load profile. Error: {err.message}</h1>
}
}

AbortController aborting ahead of time

What I want to achieve is to abort previous request after user had changed filters.
I have tried this:
const API_URL = "https://www.example.com"
const controller = new AbortController();
const signal = controller.signal;
export const fetchData = async (filters) => {
// this console.log is fired only once
console.log("Below I thought it would abort a request if ongoing, and ignore otherwise");
controller.abort();
const response = await fetch(`${API_URL}/products?${filters}`, { method: "GET", signal });
return await response.json();
}
But what happens is my request being aborted even on first invoke, kind of ahead of time.
Other thing I tried is managing AbortController like so:
let controller;
let signal;
export const fetchData = async (filters) => {
if (controller) {
console.log("aborting")
controller.abort();
controller = null;
fetchData(filters); // won't work until I invoke this function here recursively
// and I expected something like
// controller = new AbortController();
// signal = controller.signal;
// ... then the rest of the function would work
} else {
controller = new AbortController();
signal = controller.signal;
}
const response = await fetch(`${API_URL}/products?${filters}`, { method: "GET", signal });
console.log("fetch fulfilled")
return await response.json();
}
But the above approach wouldn't work if I don't include recursive call of fetchData because calling controller.abort() caused the whole function to throw error and not perform until the end, after the if block.
And this would leave me happy if it worked, but "fetch fulfilled" is logged out twice. Why?
When there's already a controller, you're both calling fetchData (in the if branch) and doing the fetch later; that branch doesn't terminate the function. So you end up with two fetches and two "fulfilled" messages.
Simplifying the code should sort it out (see *** comments):
let controller; // Starts out with `undefined`
export const fetchData = async (filters) => {
// Abort any previous request
controller?.abort(); // *** Note the optional chaining
controller = new AbortController(); // *** Controller for this request
const response = await fetch(`${API_URL}/products?${filters}`, {
method: "GET",
signal: controller.signal, // *** Using this controller's signal
});
console.log("fetch fulfilled");
// *** Note: You need a check `response.ok` here
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return await response.json();
};

Call two async functions in a row within React app

I have two async functions which reach out to API endpoints (Serverless Framework) - one gets and returns a token, the other gets and returns data using the token.
I'm testing these by using simple buttons, where onClick calls the functions to pull the token and the data, respectively. Click one button to get the token, which is saved to state. Then, once I see the token has been received, I click the other button to get the data. This works without any issues at all.
The problem is when I try calling them sequentially from the React app. I need to call these back-to-back when the user submits a request. I can't seem to make the code wait for the token to arrive before trying to pull the data.
The functions being called in the onClick method of the button:
const tokenBtnOnClick = () =>
{
const response = getToken().then(x => {
setToken(x.data.response.token)
})
}
const dataBtnOnClick = () =>
{
const response = getData(token, param1, param2, param3).then(x => {
setData(x.data.response)
})
}
Async functions:
export async function getToken()
{
const apiUrl = `${BASE_URL}/handler/getToken`
const axios = require('axios').default
let response
try
{
response = await axios.get(apiUrl)
}
catch (e)
{
console.log(e)
}
if (response)
{
return response
}
else
{
return ''
}
}
export async function getData(token, param1, param2, param3)
{
const apiUrl = `${BASE_URL}/handler/getData?token=${token}&param1=${param1}&param2=${param2}&param3=${param3}`
const axios = require('axios').default
let response
try
{
response = await axios.post(apiUrl)
}
catch (e)
{
console.log(e)
}
if (response)
{
return response
}
else
{
return ''
}
}
I've tried calling this getBoth() function in a single button's onClick:
async function getBoth()
{
const tokenResponse = await tokenBtnOnClick().then(x => setToken(x.data.response.token))
const dataResponse = await dataBtnOnClick().then(x => setData(x.data.response))
}
But even though it's an async function that uses await on both lines, I always get the same TypeError because dataBtnOnClick is called immediately, without actually waiting for the token to come in. When I run this code, tokenBtnOnClick is called, the app crashes due to a TypeError, and then the token comes in and is logged and saved to state.
I've also tried this: (where getData is exactly as above, but now accepts token as a paramter rather than using the state variable)
async function getBoth()
{
const response = await getToken().then(x => getData(x.data.response.token))
}
index.js?bee7:59 Uncaught (in promise) TypeError: Cannot read
properties of undefined (reading 'then')
How do I get this to actually wait for the token to come in before trying to pull the data?
You are calling setToken and expection token to be updated immediately, but setToken will be asynchronously applied.
Can you useEffect to solve your problem?
useEffect(() => {
getData(token, param1, param2, param3).then(x => {
setData(x.data.response)
})
}, [token])
Try this
const tokenBtnOnClick = () =>{
setToken(getToken())
}
const dataBtnOnClick = () =>{
setData(getData(token, param1, param2, param3))
}
and
const axios = require('axios').default
export async function getToken()
let apiUrl = `${BASE_URL}/handler/getToken`
{
let response = await axios.get(apiUrl)
return response.data.token;
//i don't know exactly what the api returns so it may be diferent
}
export async function getData(token, param1, param2, param3)
{
let apiUrl = `${BASE_URL}/handler/getData? token=${token}&param1=${param1}&param2=${param2}&param3=${param3}`
let response = await axios.post(apiUrl)
return response.data.response;
}
and in your getBoth() just call them because the functions are asynchronous the code will only move forward after them are finished
getBoth(){
setToken(getToken())
setData(getData(token, param1, param2, param3))
}

Axios create shorter duration for ETIMEDOUT

I have a server application (we'll call ServerApp1) which is going to be running on an Azure VM instance. I have a separate server (which we'll call ServerApp2) on a different machine which will be communicating with ServerApp1 and a separate client. The Azure VM is going to be spun up and/or down depending on need, so it's quite possible that the VM (and thus ServerApp1) aren't even alive to respond to request from ServerApp2. My client is polling ServerApp2 to ask for the status of ServerApp1, but if the VM is currently down then that request hangs for like 20 seconds before issuing an error with code ETIMEDOUT. What I'd like is for ServerApp2 to make the request to ServerApp1 to see if it's alive, but after about 1 or 2 seconds of not getting a response to then simply stop and tell the client that's it's not currently running. I thought I could get away with adding a {timeout:2000} parameter to my axios call, but this doesn't seem to change the behavior in any noticeable way.
Here's the function that gets called when the client asks ServerApp2 what the status is of ServerApp1:
router.get('/getCurrentConsoleStatus', function(req, res) {
async function getStatus() {
try {
const result = await consoleDataService.getConsoleStatus();
if (result.message === 'Begin listen for job.') {
console.log('The app is ready!');
} else {
console.log('The console app is not ready');
}
} catch (error) {
console.log(`Error communicating with console app: ${error}`);
}
}
getStatus();
});
I have a function which creates the root Axios object:
var axios = require('axios');
module.exports = axios.create({
baseURL: 'baseURL',
timeout: 1000,
headers: {
'Content-type': 'application/json'
}
});
And then the function that gets called in consoleDataService.getConsoleStatus() looks like this:
exports.getConsoleStatus = async function() {
const res = await axios({
method: 'get',
url: '/status'
});
return res.data;
};
Thanks to #JonEdwards for the suggestion to use Promise.race. I ended up solving the issue with this function which tries to take the first promise which resolves first.
exports.getConsoleStatus = () => {
var sleep = function() {
return new Promise(resolve => {
setTimeout(function() {
resolve({ status: false });
}, 2000);
});
};
var fetch = async function() {
const res = await http({
method: 'get',
url: '/status'
});
return new Promise(resolve => {
resolve(res.data);
});
};
async function getStatus() {
const asyncFunctions = [sleep(), fetch()];
const result = await Promise.race(asyncFunctions);
return result;
}
return getStatus();
};

Axios cancel token cancelling request before even called

So I am implementing axios call cancelation in the project. Right now looking at axios documentation it seems pretty straight forward https://github.com/axios/axios#cancellation
So I did define variables on the top of my Vue component like
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
obviously on top of that is import axios from 'axios';
Then I have a method of fetching the API
On the top of the method I want to cancel out the request in case it is running so the last one cancels out if the user spams the filtering.
async fetchPartners(inputToClear) {
source.cancel();
...
try {
const response = await axios.get(`../partners?limit=1000${this.createRequestString()}`, {
cancelToken: source.token
});
// Here you can see I did add the cancelToken to the request
this.partners = response.data.data;
} catch (error) {
if (axios.isCancel(error)) {
console.log('Request canceled', error.message);
}
const fetchErrors = this.utilGlobalHandleErrorMessages(error);
this.utilGlobalDisplayMessage(fetchErrors.message, { type: 'error' });
return [];
} finally {
...
}
},
So it is pretty straight forward, just took the code from axios documentation I gave you above, it should be working by logic. But what is actually happening, it doesn't even allow me to fetch the call, it is already cancelled out before I can call it. On console it shows me
Request canceled undefined
It just catches the error as if I am cancelling the call, but how can it be, because I am source.cancel() before the call.
Anyone has any idea?
I hope you should throttle your requests instead of canceling the request.
Could you please try the following if throttle does not suit your requirement?
const CancelToken = axios.CancelToken;
let source;
async fetchPartners(inputToClear) {
if(source){
source.cancel();
}
...
source = CancelToken.source();
try {
const response = await axios.get(`../partners?limit=1000${this.createRequestString()}`, {
cancelToken: source.token
});
// Here you can see I did add the cancelToken to the request
this.partners = response.data.data;
} catch (error) {
if (axios.isCancel(error)) {
console.log('Request canceled', error.message);
}
const fetchErrors = this.utilGlobalHandleErrorMessages(error);
this.utilGlobalDisplayMessage(fetchErrors.message, {
type: 'error'
});
return [];
} finally {
...
}
}

Categories