I want my fetch request to have some sort of retry system if it somehows fails based on the HTTP code of the response (for example: not 200). It looks something like this:
fetch('someURLWithAJSONfile/file.json')
.then(function (res) {
console.log(res.status);
if (res.status !== 200) {
console.log("There was an error processing your fetch request. We are trying again.");
// Recursive call to same fetch request until succeeds
} else {
return res.json();
}
}).then(function (json) {
data = json;
}).catch(function (err) {
console.log(`There was a problem with the fetch operation: ${err.message}`);
});
Is there a way to put the fetch request inside a custom Promise and make it call itself after checking its http response status?
Here the simple ES6 solution (since you are using fetch). The limit option means how many times you want to try your request.
var doRecursiveRequest = (url, limit = Number.MAX_VALUE) =>
fetch(url).then(res => {
if (res.status !== 200 && --limit) {
return doRecursiveRequest(url, limit);
}
return res.json();
});
doRecursiveRequest('someURLWithAJSONfile/file.json', 10)
.then(data => console.log(data))
.catch(error => console.log(error));
You can do this by wrapping your call to fetch in a named function that returns the promise created by fetch. Consider:
function fetchWithRetry(url, retryLimit, retryCount) {
retryLimit = retryLimit || Number.MAX_VALUE;
retryCount = Math.max(retryCount || 0, 0);
return fetch(url).then(function (res) {
console.log(res.status);
if (res.status !== 200 && retryCount < retryLimit) {
console.log("There was an error processing your fetch request. We are trying again.");
return fetchWithRetry(url, retryLimit, retryCount + 1);
} else {
return res.json();
}
});
}
fetchWithRetry('someURLWithAJSONfile/file.json', 10).then(function (json) {
data = json;
}).catch(function (err) {
console.log(`There was a problem with the fetch operation: ${err.message}`);
});
This code wraps your existing call and takes advantage of closure scope to maintain a retry limit and count which are both optional. You then call the fetchWithRetry function with a URL just like you did your previous call to fetch. If you do not pass a retry limit it will continue endlessly. The final retryCount variable is really only used for recursion purposes and is meant to be called internally.
Related
This question already has answers here:
nodejs - How to promisify http.request? reject got called two times
(6 answers)
Closed 1 year ago.
const http = require("http");
async function sendRequest(url) {
url = new URL(url);
const requestDetails = {
'hostname': url.hostname,
'port': url.port || 80,
'path': url.pathname,
'method': 'GET'
};
const req = await new Promise((resolve, reject) => {
const request = http.request(requestDetails, response => {
const status = response.statusCode;
if (status === 200 || status === 201) {
console.log("SUCCESS");
resolve(request);
} else {
console.log("ERROR");
reject(`Status code returned was ${status}`);
}
});
});
req.end();
}
sendRequest('http://httpbin.org/get');
It works when req.end() is inside the promise, but after passing the request out then execute req.end(), the console is just holding without any response. I tried to compare "req === request" by a middle variable, it returned true. Why doesn't moving end() out work? Shouldn't these two object the same?
The purpose of the req.end() is to finish the request. We might be cautious that if any body part is unsent or might in progress, it will flush them in the stream, or if any request is chunked, this will send to terminating.
I have implemented your same code in a bit different and cleaner way. Below way might help to reuse the same code for multiple apis.
const http = require("http");
/**
* #description call the http request
*/
async function doHttp(requestDetails){
return new Promise((resolve, reject)=>{
http.request(requestDetails, response => {
const status = response.statusCode;
response.setEncoding("utf-8");
if (status === 200 || status === 201) {
console.log("SUCCESS");
response.on('data', data => {
return resolve(data);
});
} else {
console.error("ERROR");
return reject(new Error("emptyData"));
}
}).on('error', (err) => {
// Catch the error if occured in request
console.error(`problem with request: ${e.message}`);
return reject(err);
}).end();
});
}
/**
* #description sending the request
*/
async function doSend(url) {
url = new URL(url);
const requestDetails = {
'hostname': url.hostname,
'port': url.port || 80,
'path': url.pathname,
'method': 'GET'
};
const data = await doHttp(requestDetails)
console.log(data);
}
doSend('http://httpbin.org/get');
At last, we could say req.end() is required to finish up any request. It completely depends on us, how we can implement a method.
An alternate solution might be this native https module is such as Axios, superagent, got, node-fetch. They provide a wrapper over the native nodejs code which might help us to control to handle an error and response.
You should move the request.end call inside the promise otherwise it just newer gets called because you will be waiting for a promise that is newer resolved because the request is not send.
Also you should reject the promise in case request object emits error event.
INTRODUCTION
I am implementing a function for making any kind of https request to any endpoint (using the https native module). When I make a request to a specific API I get an error response in JSON format. Like this:
{
"error": {
"code": 404,
"message": "ID not found"
}
}
How can I handle this kind of errors? At a first moment, I supposed that they were handled in
request.on("error", (err) => {
reject(err);
});
HTTPs Request function code
I have comment '<---------' in the relevant parts of the code
const https = require("https");
exports.httpsRequest = function (options, body = null) {
/*
This function is useful for making requests over the HTTPs protocol
*/
return new Promise((resolve, reject) => {
const request = https.request(options, (response) => {
// Get the response content type
const contentType =
response.headers["content-type"] &&
response.headers["content-type"].split(";")[0];
// Cumulate data
let chuncks = [];
response.on("data", (chunck) => {
chuncks.push(chunck);
});
response.on("end", () => {
// Concat all received chunks
let response = Buffer.concat(chuncks);
// Some responses might be in JSON format...
if (contentType === "application/json") {
// Jsonify the response
response = JSON.parse(response);
}
// (For the future) TODO - Check and parse more content types if needed.
// Resolve the promise with the HTTPs response
resolve(response); // <--------- The JSON format error responses are resolved too!!
});
});
// Reject on request error
request.on("error", (err) => {
// <------------- At a first moment, I supposed that all error responses were handled in this part of the code
reject(err);
});
// Write the body
if (body) {
request.write(body);
}
// Close HTTPs connection.
request.end();
});
};
Question
Why the error response is not handled in request.on("error", ...) ?
Thank you. I would appreciate any help or suggestion.
You need to create a different code path for when the content type isn't what you were expecting in which you call reject() and you also need to try/catch around JSON parsing errors so you can properly catch them and reject on them too. You can solve those issues with this code:
exports.httpsRequest = function (options, body = null) {
/*
This function is useful for making requests over the HTTPs protocol
*/
return new Promise((resolve, reject) => {
const request = https.request(options, (response) => {
// Get the response content type
const contentType =
response.headers["content-type"] &&
response.headers["content-type"].split(";")[0];
// Cumulate data
let chuncks = [];
response.on("data", (chunck) => {
chuncks.push(chunck);
});
response.on("end", () => {
// Concat all received chunks
let response = Buffer.concat(chuncks);
// Some responses might be in JSON format...
if (contentType === "application/json") {
try {
// Jsonify the response
response = JSON.parse(response);
resolve(response);
return;
} catch(e) {
reject(e);
return;
}
}
reject(new Error("Not JSON content-type"))
});
});
// Reject on request error
request.on("error", (err) => {
reject(err);
});
// Write the body
if (body) {
request.write(body);
}
// Close HTTPs connection.
request.end();
});
};
FYI, libraries such as got() and others listed here, all do this work for you automatically and have a lot of other useful features. You don't really need to build this yourself.
I'm currently working on a script that sends requests to a 3rd party website, but after sometime the cookies are invalid & it throws a 403
So I've got my script to send the request, if it throws the 403 then we send VALID cookies and then RESEND the request.
However it appears that the state.device_info remains undefined after updating it inside the catch function?
(async () => {
let state = {
'access_denied': false,
'cookies': {}
};
try {
await getCookies(state);
state.device_info = await getInfo(state).catch(async (error) => {
let localError = handleError(error, state);
if(localError === 'access_denied') {
state.access_denied = true;
//now lets unlock the request & send it again!
let post = await sendValidCookies(state);
if(post.data.success === true) {
//update state.device_info with WORKING request!
state.device_info = await getInfo(state).catch(async (error) => {
console.log('Damn we got another error!');
console.log(error);
})
if(state.device_info.status === 200) {
console.log('We got our info info using the unlocked request!');
state.access_denied = false;
}
}
}
});
if(state.device_info === undefined || state.access_denied === true) {
console.log('We have an undefined value!');
console.log(state.device_info); //undefined
console.log(state.access_denied); //false
return false;
}
} catch(error) {
console.log('major error!');
}
})();
Instead of defining it inside catch, just return the value. This returned value will be assigned to state.device_info
state.device_info = await getInfo(state).catch(async (error) => {
let localError = handleError(error, state);
if(localError === 'access_denied') {
state.access_denied = true;
//now lets unlock the request & send it again!
let post = await sendValidCookies(state);
if(post.data.success === true) {
//Get result from WORKING request
const result = await getInfo(state).catch(async (error) => {
console.log('Damn we got another error!');
console.log(error);
});
if(state.device_info.status === 200) {
console.log('We got our info info using the unlocked request!');
state.access_denied = false;
}
return result;
}
}
});
Since you're using the await keyword in front of your method call getInfo(state) and you're calling .catch(...) right after, I assume getInfo(state) returns a Promise.
There are two ways you can handle Promise in JavaScript:
By explicitly call .then(result => {...}) while catching errors with .catch(error => {...}).
By putting the await keyword in front of your method call. By using this syntax, and if you want to catch errors (which you should do), you need to use the try { } catch (error) { } syntax.
I've never mix those two methods like you did and I think this might be the origin of the issue you're experiencing.
For your case, it should look like this:
try {
state.device_info = await getInfo(state);
} catch (error) {
// Your logic here...
}
I need to do several attempts on the async function getDBfileXHR, but I don't know how to handle this. I am new to chaining promises. Should I chain them like this?
return getDBfileXHR(dbUrl(), serverAttempts)
.then(function () { // success
console.log('-basic XHR request succeeded.');
return dbReadyDeferred.promise;
})
.catch(function (){
return getDBfileXHR(dbUrl(), serverAttempts)
.then(function (){
console.log('-basic XHR request succeeded after second attempt.');
return dbReadyDeferred.promise;
})
.catch(function () { // error
console.log("-basic XHR request failed, falling back to local DB file or localStorage DB...");
return fallbackToLocalDBfileOrLocalStorageDB();
});
})
or like that :
return getDBfileXHR(dbUrl(), serverAttempts)
.then(function () { // success
console.log('-basic XHR request succeeded.');
return dbReadyDeferred.promise;
})
.catch(function (){
if (typeof serverAttempts !== "undefined") serverAttempts++;
console.log('on passe dans le catch, serverAttempts = ', serverAttempts)
if (serverAttempts < 2) {
return getDBfileXHR(dbUrl(), serverAttempts)
.then(function () { // success
console.log('-basic XHR request succeeded.');
return dbReadyDeferred.promise;
})
.catch(function (){
console.log("-basic XHR request failed, falling back to local DB file or localStorage DB...");
return fallbackToLocalDBfileOrLocalStorageDB();
})
} else {
console.log("-basic XHR request failed, falling back to local DB file or localStorage DB...");
return fallbackToLocalDBfileOrLocalStorageDB();
}
})
This second code seems to work, but I am not sure it is best practices.
A simple and flexible solution involves creating a helper - benefit, reusable for anything that requires retrying promises:
var retryP = (fn, retry) => fn(retry).catch(err => (!isNaN(retry) && retry > 0) ? retryP(fn, retry - 1) : Promise.reject(err));
This generic function will retry fn for at most attempts number of times, passing 1 will retry once, i.e. make two attempts
your function can then be written:
var serverAttempts = 1;
// this is should be the retry attempts,
// so 0 is try at most once, 1 is at most twice etc
// argument n will be the number of retries "in hand",
// so it counts down from the passed in value to 0
return retryP(n => getDBfileXHR(dbUrl(), serverAttempts - n), serverAttempts)
.then(() => {
console.log('-basic XHR request succeeded after second attempt.');
return dbReadyDeferred.promise;
})
.catch(() => {
console.log("-basic XHR request failed, falling back to local DB file or localStorage DB...");
return fallbackToLocalDBfileOrLocalStorageDB();
});
If you aren't comfortable with ES2015+ syntax
helper:
var retryP = function retryP(fn, retry) {
return fn(retry).catch(function (err) {
return !isNaN(retry) && retry > 0 ? retryP(fn, retry - 1) : Promise.reject(err);
});
};
code:
var serverAttempts = 1;
return retryP(function (n) {
return getDBfileXHR(dbUrl(), serverAttempts - n);
}, serverAttempts)
.then(function () {
console.log('-basic XHR request succeeded after second attempt.');
return dbReadyDeferred.promise;
})
.catch(function () {
console.log("-basic XHR request failed, falling back to local DB file or localStorage DB...");
return fallbackToLocalDBfileOrLocalStorageDB();
});
I have an HTTP API that returns JSON data both on success and on failure.
An example failure would look like this:
~ ◆ http get http://localhost:5000/api/isbn/2266202022
HTTP/1.1 400 BAD REQUEST
Content-Length: 171
Content-Type: application/json
Server: TornadoServer/4.0
{
"message": "There was an issue with at least some of the supplied values.",
"payload": {
"isbn": "Could not find match for ISBN."
},
"type": "validation"
}
What I want to achieve in my JavaScript code is something like this:
fetch(url)
.then((resp) => {
if (resp.status >= 200 && resp.status < 300) {
return resp.json();
} else {
// This does not work, since the Promise returned by `json()` is never fulfilled
return Promise.reject(resp.json());
}
})
.catch((error) => {
// Do something with the error object
}
// This does not work, since the Promise returned by `json()` is never fulfilled
return Promise.reject(resp.json());
Well, the resp.json promise will be fulfilled, only Promise.reject doesn't wait for it and immediately rejects with a promise.
I'll assume that you rather want to do the following:
fetch(url).then((resp) => {
let json = resp.json(); // there's always a body
if (resp.status >= 200 && resp.status < 300) {
return json;
} else {
return json.then(Promise.reject.bind(Promise));
}
})
(or, written explicitly)
return json.then(err => {throw err;});
Here's a somewhat cleaner approach that relies on response.ok and makes use of the underlying JSON data instead of the Promise returned by .json().
function myFetchWrapper(url) {
return fetch(url).then(response => {
return response.json().then(json => {
return response.ok ? json : Promise.reject(json);
});
});
}
// This should trigger the .then() with the JSON response,
// since the response is an HTTP 200.
myFetchWrapper('http://api.openweathermap.org/data/2.5/weather?q=Brooklyn,NY').then(console.log.bind(console));
// This should trigger the .catch() with the JSON response,
// since the response is an HTTP 400.
myFetchWrapper('https://content.googleapis.com/youtube/v3/search').catch(console.warn.bind(console));
The solution above from Jeff Posnick is my favourite way of doing it, but the nesting is pretty ugly.
With the newer async/await syntax we can do it in a more synchronous looking way, without the ugly nesting that can quickly become confusing.
async function myFetchWrapper(url) {
const response = await fetch(url);
const json = await response.json();
return response.ok ? json : Promise.reject(json);
}
This works because, an async function always returns a promise and once we have the JSON we can then decide how to return it based on the response status (using response.ok).
You would error handle the same way as you would in Jeff's answer, however you could also use try/catch, an error handling higher order function, or with some modification to prevent the promise rejecting you can use my favourite technique that ensures error handling is enforced as part of the developer experience.
const url = 'http://api.openweathermap.org/data/2.5/weather?q=Brooklyn,NY'
// Example with Promises
myFetchWrapper(url)
.then((res) => ...)
.catch((err) => ...);
// Example with try/catch (presuming wrapped in an async function)
try {
const data = await myFetchWrapper(url);
...
} catch (err) {
throw new Error(err.message);
}
Also worth reading MDN - Checking that the fetch was successful for why we have to do this, essentially a fetch request only rejects with network errors, getting a 404 is not a network error.
I found my solution at MDN:
function fetchAndDecode(url) {
return fetch(url).then(response => {
if(!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} else {
return response.blob();
}
})
}
let coffee = fetchAndDecode('coffee.jpg');
let tea = fetchAndDecode('tea.jpg');
Promise.any([coffee, tea]).then(value => {
let objectURL = URL.createObjectURL(value);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(e => {
console.log(e.message);
});
Maybe this option can be valid
new Promise((resolve, reject) => {
fetch(url)
.then(async (response) => {
const data = await response.json();
return { statusCode: response.status, body: data };
})
.then((response) => {
if (response.statusCode >= 200 && response.statusCode < 300) {
resolve(response.body);
} else {
reject(response.body);
}
})
});