How to cancel http request properly in Node.js? - javascript

I need to implement a cancel-able client-side HTTP request in Node.js, without using external libraries. I'm giving a Promise object - cancellationPromise - which gets rejected when the cancellation is externally requested. This is how I know I may need to call request.abort().
The question is, should I be calling request.abort() only if https.request is still pending and response object is not yet available?
Or, should I be calling it even if I already got the response object and am processing the response data, like in the code below? In which case, will that stop any more response.on('data') events from coming?
async simpleHttpRequest(url, oauthToken, cancellationPromise) {
let cancelled = null;
let oncancel = null;
cancellationPromise.catch(error => {
cancelled = error; oncancel && oncancel(error) });
try {
const response = await new Promise((resolve, reject) => {
const request = https.request(
url.toString(),
{
method: 'GET',
headers: { 'Authorization': `Bearer ${oauthToken}` }
},
resolve);
oncancel = error => request.abort();
request.on('error', reject);
request.end();
});
if (cancelled) throw cancelled;
// do I need "oncancel = null" here?
// or should I still allow to call request.abort() while fetching the response's data?
// oncancel = null;
try {
// read the response
const chunks = await new Promise((resolve, reject) => {
response.on('error', reject);
const chunks = [];
response.on('data', data => chunks.push(data));
response.on('end', () => resolve(chunks));
});
if (cancelled) throw cancelled;
const data = JSON.parse(chunks.join(''));
return data;
}
finally {
response.resume();
}
}
finally {
oncancel = null;
}
}

It depends what you want to achieve by aborting a request.
Just a bit of background. HTTP 1 is not able to "cancel" a request it sends it and then waits for the response. You cannot "roll back" the request you did. You need a distributed transaction to do so. (Further reading.) As the MDN developer document states:
The XMLHttpRequest.abort() method aborts the request if it has already been sent. When a request is aborted, its readyState is changed to XMLHttpRequest.UNSENT (0) and the request's status code is set to 0.
Basically you stop the response from being processed by your application. The other application will probably (if you called abort() after it was sent to it) finish its processing anyways.
From the perspective of the question:
The question is, should I be calling request.abort() only if https.request is still pending and response object is not yet available?
TL.DR.: It only matters from the point of view of your application. As I glance at your code, I think it will work fine.

Related

Axios connection timeout

I see a lot of answers and none of them work for me. I am implementing retry code in the browser, where if the API hasn't responded in 4000ms it retries.
The problem is I want to do this for POST requests that are not idempotent, and the response state in chrome dev tools (whether it succeeds or fails) does NOT match axios or my implemented logic of when a timeout occurs.
This results in POST requests calling twice successfully on the server even though the connection throws an error within my axios code. It's a race condition somewhere, I'm assuming the time between axios connects and when it is able to set the result of the response.
I've tried default axios timeout which doesn't work, as that is a response timeout.
I've also tried to implement a connection timeout and I still am encountering the same issue.
The issue starts occuring if I set the connTimeout to be right in the ballpark of how long it takes the server to response on average, +/- a few ms. I feel like when a request is cancelled, somehow it's not checking if the connection actually succeeded or not before attempting to cancel.
I'd do it myself (before calling source.cancel(), but I'm not sure what I can read to get the state. The only thing I see there is an unresolved promise)
const makeRequest = async (args, connTimeout, responseTimeout) => {
const source = axios.CancelToken.source();
const argsWithToken = {
...args,
cancelToken: source.token,
};
const api = buildAxios(responseTimeout);
const timeout = setTimeout(source.cancel, connTimeout);
return api(argsWithToken).then(result => {
clearTimeout(timeout);
return result;
});
};
const handleRetries = (args, maxRetries) => (
new Promise(async (resolve, reject) => { /* eslint-disable-line */
let retries = 0;
let success = false;
while (!success && retries < maxRetries) {
try {
const result = await makeRequest(args, 300, 30000); /* eslint-disable-line */
success = true;
resolve(result);
} catch (err) {
retries += 1;
// console.log(`Error making ${args.method} request to ${args.url}, retrying... #${retries}`);
}
}
// line below is included to prevent process leaks
if (!success) reject(new Error(`Retried ${retries} times and still failed: ${args.url}`));
})
);
handleRetries({url: '/settings', method: 'get'}, 3)

How do I cancel a ongoing GET request with axios

I have an inputfield, onChange it sends my value of the inputfield to an API. So, the api will start fetching all the data. but when I continue typing again, I want that previous request to be canceled.
I'm using axios for making my request and tried looking at the documentation but I can't seem to figure out how it really works, can someone explain how to do this?
Here is my function that gets called by every new input:
const onChange = (value) => {
setTimeout(async() => {
let result = []
if (y === "keyword") result = await AutoSuggestionKeyword({
value: value
});
else {
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
await axios.get(`https://${api_uri}/${value.toLowerCase()}`)
.catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
}).then(resp => {
console.log(resp)
});
source.cancel();
}
}, 500)
}
You need to provide a cancelToken in your request,
axios.get(`https://${api_uri}/${value.toLowerCase()}`, {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
I don't think you can cancel an HTTP request, but what you can do is wrap it in debounce function, debounce function wait for a certain time before calling the callback or function you wrap or pass on it.
you can simply google debounce and it will give you articles or npm packages that you can use.
I think this article also has the same issue you are trying to resolve
Happy coding
Edit 1: yeah so you can cancel the http request see comment below

.then() fires before promise resolves

In my vanilla js project I have a following chain of promises:
API.POST({link to login endpoint}, "email=foo&pass=bar")
.then(() => User.initiate(API))
.then(() => console.log(User.name || 'wat'));
API object has POST and GET methods both looking the same, except the request type:
GET (url, params = null) {
console.log("GET start");
return new Promise((resolve,reject) => {
this._request('GET', url, params)
.then(result => resolve(result));
});
}
POST (url, params = null) {
return new Promise((resolve,reject) => {
this._request('POST', url, params)
.then(result => resolve(result));
});
}
... and a _request method responsible for sending the request:
_request (type, url, params = null) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.open(type,url,true);
xhr.withCredentials = true;
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
xhr.onload = function(){
if (this.status == 200) {
console.log(type + ' done');
resolve(this.response);
} else {
reject(this.response._status.code);
}
};
xhr.send(params);
});
}
App's User object provides "initiate" method that calls the API to check if user is logged in. With positive response API returns an _embedded.user object, that in the next step is used to populate app's User properties:
initiate(API) {
API.GET({link to authorization check endpoint})
.then(authData => this.regenerate(authData._embedded.user));
},
regenerate(userData) {
this.name = userData.name;
// and so on, for each User property
return new Promise((resolve,reject) => {
resolve(this);
});
}
What I expect to happen is:
Request to API to log in is sent (this is just to skip the actual logging in process that is irrelevant to the current work)
API returns cookie allowing for further testing as an logged user
Request to API is sent to ask if user is authenticated
API responds with confirmation and _embedded.user object
App's User object's properties get populated by data from API response
User.name is logged in console
Step 6 though fires between steps 3. and 4. and I can't find the reason why. My console looks following (notice the console.logs in API object code above):
POST done
GET start
wut
GET done
What can be the reason of this? Thanks in advance.
Missing a return in initiate()
initiate(API) {
return API.GET({link to authorization check endpoint})
//^^ return the promise
.then(authData => this.regenerate(authData._embedded.user));
}
Also using a promise anti-pattern in your GET and POST methods. There is no need to create a new promise in each since _request() already returns a promise
All you need is:
GET (url, params = null) {
return this._request('GET', url, params);
}
POST (url, params = null) {
return this._request('POST', url, params);
}
For more detailed explanation see What is the explicit promise construction antipattern and how do I avoid it?
Rather than using XMLHttpRequest you may also want to look at using the more recent fetch() API which has built in promises and better error handling

Javascript: Promise returning instantly, rather than waiting for asynchronous process to finish

Essentially when i call my function getToken() it should return the bearer + token from the api.
The problem I have is that due to the asynchronous process that happens, the data is not returned instantly; so in reading the following resource:
How do I return the response from an asynchronous call?
My understanding is that I need to return my response in the form of a promise, and set a timeout to ensure that the return accounts for the time it takes for the server to send back my request in the form of a response.
var request = require('request-promise');
var time = require('timers');
class Auth {
getToken() {
let options = {
method: 'POST',
uri: 'https://example.com/service/ep',
body: {
username: 'someUser',
password: 'somePass'
},
json: true
}
request(options)
.then(function (body) {
// console.log(body)
return new Promise((resolve) => {
time.setTimeout(() => {
resolve(body)
},3000)
});
})
.catch(function (err) {
return err
});
}
}
module.exports = new Auth
Unfortunately when i run my program in the node repel, it returns nothing and it does not appear to wait; of course when i log my response 'console.log(body)', it appears meaning there must be something wrong with how i'm returning my promise; i'm quite new to the likes of promises.
Could use with a second pair of eyes.
My understanding is that I need to return my response in the form of a promise, and set a timeout to ensure that the return accounts for the time it takes for the server to send back my request in the form of a response.
No. You need to return a promise (request already gives you one) and then the code you return the promise to needs to expect a promise (and call then() on it to get the data).
You don't need any time delays.
var request = require('request-promise');
var time = require('timers');
class Auth {
getToken() {
let options = {
method: 'POST',
uri: 'https://example.com/service/ep',
body: {
username: 'someUser',
password: 'somePass'
},
json: true
}
return request(options);
}
}
module.exports = new Auth
const auth = require("Auth");
auth.getToken().then(function (data) {
console.log(data);
});

Axios Request Interceptor wait until ajax call finishes

I have an request interceptor for axios calls. It checks my jwt token and call for refresh if necessary.
axios.interceptors.request.use((config) =>{
const state = store.getState(); // get renewed state
const time = Math.floor( new Date().getTime() / 1000 );
if(
! state.app.jwtRefreshOnRequest
&& time >= state.jwt.expires - 120
&& state.jwt.refresh_before > time
){ // expiring in 2 min. refresh
//dispatch({type: 'JWT_REFRESH_REQUEST'});
axios.get( API_BASE_URL + '/auth/refresh')
.then(function(response){
// dispatch({type: 'JWT_REFRESH_SUCCESS', payload: response.data});
axios(config).then(resolve, reject);
})
.catch(function(err){
reject(err);
});
}
return config;
});
This code calls the refresh correctly and saves the new token but the original call doesn't holds until the interceptor request is done, so the expired token is used.
So, I guess I need to make synchronous call from interceptor.
Avoid synchronous calls for HTTP requests, as they just make your application hang.
What you need to do here is make the calling code asynchronous - the general rule with anything callback, promise or async related is that once you are async everything needs to be async.
Here, axios.get returns a Promise - an object that keeps track of the asynchronous HTTP request and resolves once it has finished. You need to return that, rather than the config.
We do that by returning a new Promise - if an HTTP request for a new token is required it waits for it, if no it can resolve immediately.
axios.interceptors.request.use(config =>
new Promise((resolve, reject) => {
// ... your code ...
axios.get( API_BASE_URL + '/auth/refresh')
.then(response => {
// Get your config from the response
const newConfig = getConfigFromResponse(response);
// Resolve the promise
resolve(newConfig);
}, reject);
// Or when you don't need an HTTP request just resolve
resolve(config);
})
});
Whenever you see that then you're dealing with Promise, and once you are everything needs to return a Promise.
This is much easier if you can use async/await - new keywords supported by modern browsers and transpilable if you need to support legacy users. With these you can just put the Promise call inline with the await keyword.
axios.interceptors.request.use(async config =>
// ... your code ...
if(/* We need to get the async token */) {
const response = await axios.get( API_BASE_URL + '/auth/refresh');
config = getConfigFromResponse(response);
}
return config;
});

Categories

Resources