Abort previous remote call - javascript

I'm looking for a way to control repeated calls to the same webservice, in case of 2 identical remote requests, I want to abort the first one.
Not sure yet if I can use AbortController. It seems if I call controller.abort(); before fetch I abort the subsequent call..
<button class="download"> fetch </button>
<button class="abort"> abort </button>
<script>
var controller = new AbortController();
var signal = controller.signal;
var downloadBtn = document.querySelector('.download');
var abortBtn = document.querySelector('.abort');
downloadBtn.addEventListener('click', fetchJson);
abortBtn.addEventListener('click', function() {
controller.abort();
console.log('Download aborted');
});
function fetchJson() {
// TODO abort previous remote call here
// controller.abort(); // won't work
fetch("http://fakeapi.jsonparseronline.com/posts", {signal}).then( async response => {
console.log(await response.json())
}).catch(function(e) {
console.error(e)
})
}
</script>

You will have to create a new AbortController for each fetch, after you cancel the previous one. An AbortController's signal is set only once, similar to how you can resolve a promise only once, and it keeps the value afterwards. So if you don't renew your AbortController, the fetch will see an already-aborted signal.
let controller = new AbortController();
document.querySelector('.download').addEventListener('click', fetchJson);
document.querySelector('.abort').addEventListener('click', function() {
controller.abort();
console.log('Download aborted');
});
async function fetchJson() {
controller.abort(); // abort previous fetch here
controller = new AbortController(); // make new fetch abortable with a fresh AbortController
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
const signal = controller.signal;
try {
const response = await fetch("http://fakeapi.jsonparseronline.com/posts", {signal});
console.log(await response.json());
} catch() {
console.error(e)
}
}

Related

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

fetch().then().catch(); doesn't catch `Uncaught (in promise) DOMException: The user aborted a request.`

I have an interface on an ESP32.
I am trying to have a Connected variable show up in an interface to signal the user if he is connected to said interface or not.
I am using the following method to constantly GET the configuration from the server in order update the interface.
// get json
async function get_json(api_path, options = {}) {
// const url = api_path;
// console.log(ROOT_URL);
// console.log(api_path);
const { timeout = 8000 } = options;
const controller = new AbortController();
const timeoutID = setTimeout(() => controller.abort(), timeout);
const response = await fetch(api_path, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutID);
return response.json();
}
async function getSettings() {
get_json("/api/settings/get", {
timeout: 5000,
}).then((json_data) => {
// do something with the data
connected = true;
}).catch(function (error) {
// request timeout
console.log(error);
connected = false;
console.log(error.name === "AbortError");
});
}
Everything works dandy except the catch part.
Let's say the user changes the IP of the ESP. The ESP restarts and reconfigures to use the newly IP address. But the user stayed on the same page because connected is still set to true. In the console I get Uncaught (in promise) DOMException: The user aborted a request. for each request to getSettings() because I can't write a proper catch.
Basically, after the IP is changed, getSettings() tries to GET from a wrong address, so it's normal to throw some sort of error which I should catch and change the Connected variable to false and update it in the interface so that the user can go/move/navigate to the IP Address they have just inputted.
Edit:
This is how I update connected in the interface:
// check if connected to ESP
function connectedStatus() {
let conn = document.getElementById("connected");
conn.innerHTML = connected ? "Yes" : "No";
}
setInterval(connectedStatus, 500);
"Doesn't get_json() return a PROMISE?" - All async function does return promise, But your getSettings function doesn't wait for get_json to become resolved.
let delay = ms => new Promise(ok => setTimeout(ok, ms));
async function asyncFn() {
delay(2000).then(() => {
console.log("wait! 2 secs...")
});
console.log("thanks for waiting")
}
asyncFn()
As you can see "thanks for waiting" print first,
So our guess is that, getSettings indeed modify connected state but not immediately but later, But your getSettings resolve immediately, and you didn't wait for state (connected) change.
So you need to wait for any changes,
let delay = ms => new Promise(ok => setTimeout(ok, ms));
let state;
async function asyncFn() {
await delay(2000).then(() => {
state = true
console.log("wait! 2 secs...")
});
console.log("thanks for waiting")
}
asyncFn().then(() => {
console.log("state:", state)
})

How do I correctly test asynchronous stream error event handling with mocha/sinon/chai?

I am testing an async method that returns some data from a web request using the native https.request() method in NodeJS. I am using mocha, chai, and sinon with the relevant extensions for each.
The method I'm testing essentially wraps the boilerplate https.request() code provided in the NodeJS docs in a Promise and resolves when the response 'end' event is received or rejects if the request 'error' event is received. The bits relevant to discussion look like this:
async fetch(...) {
// setup
return new Promise((resolve, reject) => {
const req = https.request(url, opts, (res) => {
// handle response events
});
req.on('error', (e) => {
logger.error(e); // <-- this is what i want to verify
reject(e);
});
req.end();
});
}
As indicated in the comment, what I want to test is that if the request error event is emitted, the error gets logged correctly. This is what I'm currently doing to achieve this:
it('should log request error events', async () => {
const sut = new MyService();
const err = new Error('boom');
const req = new EventEmitter();
req.end = sinon.fake();
const res = new EventEmitter();
sinon.stub(logger, 'error');
sinon.stub(https, 'request').callsFake((url, opt, cb) => {
cb(res);
return req;
});
try {
const response = sut.fetch(...);
req.emit('error', err);
await response;
} catch() {}
logger.error.should.have.been.calledOnceWith(err);
});
This feels like a hack, but I can't figure out how to do this correctly using the normal patterns. The main problem is I need to emit the error event after the method is called but before the promise is fulfilled, and I can't see how to do that if I am returning the promise as you normally would with mocha.
I should have thought of this, but #Bergi's comment about using setTimeout() in the fake gave me an idea and I now have this working with the preferred syntax:
it('should log request error events', () => {
const sut = new MyService();
const err = new Error('boom');
const req = new EventEmitter();
req.end = sinon.fake();
const res = new EventEmitter();
sinon.stub(logger, 'error');
sinon.stub(https, 'request').callsFake((url, opt, cb) => {
cb(res);
setTimeout(() => { req.emit('error', err); });
return req;
});
return sut.fetch(...).should.eventually.be.rejectedWith(err)
.then(() => {
logger.error.should.have.been.calledOnceWith(err);
});
});
I don't like adding any delays in unit tests unless I'm specifically testing delayed functionality, so I used setTimeout() with 0 delay just to push the emit call to the end of the message queue. By moving it to the fake I was able to just use promise chaining to test the call to the logger method.

Can't make new fetch request after aborting previous

I need to change a parameters that defines what data should come from my requests, also this application needs to refresh on a regular time interval. If the user changes the parameter in the middle of an unfinished request things start to behave strange and some unexpected behavior occurs.
So my approach was to abort all previous requests before starting the new ones, but after using await controller.abort() it seems that the next requests are never triggered, Do I need to clear the signal or something like that?
const controller = new AbortController();
const async fetchData = (url, body = null) => {
let data;
const signal = controller.signal;
const headers = { ... };
response = await fetch(url, body ? {
method: "POST",
body: JSON.stringify(body),
signal,
headers
} : { headers, signal });;
data = await response.json()
return data
}
const firstData = await fetchData(url1, body1);
await controller.abort();
const secondData= await fetchData(url2, body2);
What happens is that secondData always is undefined, actually this second request never happens (looking on network traffic). If I stop source and try to run await fetchData(url2) after .abort() has executed it prompts an erros saying that Uncaught SyntaxError: await is only valid in async function or if I try to run it without await it returns a pending promise, but the actual request is nowhere to be seen in traffic tab.
Solved
Applying what was suggested on the ansewr I created wrapper on the function, to call new controllers everytime.
let controller = null;
let fetchData = null;
const initializeFetchData = () => {
const controller = new AbortController();
const async fetchData = (url, body = null) => {
let data;
const signal = controller.signal;
const headers = { ... };
response = await fetch(url, body ? {
method: "POST",
body: JSON.stringify(body),
signal,
headers
} : { headers, signal });;
data = await response.json()
return data
}
}
initializeFetchData();
const firstData = await fetchData(url1, body1);
controller.abort();
initializeFetchData();
const secondData= await fetchData(url2, body2);
You are using the sameAbortController for two different requests. After calling .abort() on theAbortController you have updated the state of it's AbortSignal which then renders the second request void.
You should use a separate AbortController for each request if you want this behavior. Of course, it is perfectly acceptable to reuse an AbortController for multiple fetch requests if you want to be able to abort all of them in one go.
A couple of other points...
.abort() is a synchronous method which returns void so you do not need the await prefix when calling .abort().
In your code example, the first request will never be aborted as you are awaiting the fetch request, which will complete before the .abort() is called.

How to cancel previous express request, so that new request gets executed?

I have this endpoint. This api takes long time to get the response.
app.get('/api/execute_script',(req,res) =>{
//code here
}
I have following endpoint which will kill the process
app.get('/api/kill-process',(req,res) => {
//code here
}
but unless first api gets response second api doesnt get executed. How to cancel previous api request and execute the second request?
You can use an EventEmitter to kill the other process, all you'll need is a session/user/process identifier.
const EventEmitter = require('events');
const emitter = new EventEmitter();
app.get('/api/execute_script', async(req,res,next) => {
const eventName = `kill-${req.user.id}`; // User/session identifier
const proc = someProcess();
const listener = () => {
// or whatever you have to kill/destroy/abort the process
proc.abort()
}
try {
emitter.once(eventName, listener);
await proc
// only respond if the process was not aborted
res.send('process done')
} catch(e) {
// Process should reject if aborted
if(e.code !== 'aborted') {
// Or whatever status code
return res.status(504).send('Timeout');
}
// process error
next(e);
} finally {
// cleanup
emitter.removeListener(eventName, listener)
}
})
app.get('/api/kill-process',(req,res) => {
//code here
const eventName = `kill-${req.user.id}`;
// .emit returns true if the emitter has listener attached
const killed = emitter.emit(eventName);
res.send({ killed })
})

Categories