How to wrap JavaScript fetch in a function - unhandled promise rejection - javascript

I'm trying to write a wrapper function for the JavaScript fetch command.
I took the example code from this post:
function fetchAPI(url, data, method = 'POST') {
const headers = {
'Authorization': `Token ${getAuthToken()}`,
};
return fetch(url, { headers, 'method': method, 'body': data })
.then(response => {
if (response.ok) {
const contentType = response.headers.get('Content-Type') || '';
if (contentType.includes('application/json')) {
return response.json().catch(error => {
return Promise.reject(new Error('Invalid JSON: ' + error.message));
});
}
if (contentType.includes('text/html')) {
return response.text().then(html => {
return {
'page_type': 'generic',
'html': html
};
}).catch(error => {
return Promise.reject(new Error('HTML error: ' + error.message));
});
}
return Promise.reject(new Error('Invalid content type: ' + contentType));
}
if (response.status === 404) {
return Promise.reject(new Error('Page not found: ' + url));
}
return response.json().then(res => {
// if the response is ok but the server rejected the request, e.g. because of a wrong password, we want to display the reason
// the information is contained in the json()
// there may be more than one error
let errors = [];
Object.keys(res).forEach((key) => {
errors.push(`${key}: ${res[key]}`);
});
return Promise.reject(new Error(errors)
);
});
}).catch(error => {
return Promise.reject(new Error(error.message));
});
};
And I'm calling it like this:
fetchAPI('/api/v1/rest-auth/password/change/', formData).then(response => {
console.log('response ', response);
});
Edit: I have modified the code to display information returned by the server if the request is ok but refused, for example because of an invalid password. You have to interrogate the response json if ok == false.
A valid URL fetch is fine. But if there is an error, I see an Unhandled Rejection (Error): error message.
Why is it that the rejects are unhandled even though they are in catch blocks? What's the secret sauce here?

The way to avoid an unhandled promise rejection, is to handle it:
fetchAPI('/api/v1/rest-auth/password/change/', formData).then(response => {
console.log('response ', response);
}).catch(error => {
// do something meaningful here.
});;

Related

Why is it not waiting for an awaited Promise.all to resolve?

I previously had working code that inefficiently called awaits on every element of an iterable. I'm in the process of refactoring to use Promise.All. However, my code is not waiting for the Promise.All to resolve before executive further code.
Specficially, the purgeRequestPromises line executes before the initial Promise.All resolves. I'm not sure why that is? retrieveSurrogateKey is an async function, so its return line will be wrapped in a resolved promise.
try {
//retrieve surrogate key associated with each URL/file updated in push to S3
const surrogateKeyPromises = urlArray.map(url => this.retrieveSurrogateKey(url));
const surrogateKeyArray = await Promise.all(surrogateKeyPromises).catch(console.log);
//purge each surrogate key
const purgeRequestPromises = surrogateKeyArray.map(surrogateKey => this.requestPurgeOfSurrogateKey(surrogateKey));
await Promise.all(purgeRequestPromises);
// GET request the URLs to warm cache for our users
const warmCachePromises = urlArray.map(url => this.warmCache(url));
await Promise.all(warmCachePromises)
} catch (error) {
logger.save(`${'(prod)'.padEnd(15)}error in purge cache: ${error}`);
throw error
}
async retrieveSurrogateKey(url) {
try {
axios({
method: 'HEAD',
url: url,
headers: headers,
}).then(response => {
console.log("this is the response status: ", response.status)
if (response.status === 200) {
console.log("this is the surrogate key!! ", response.headers['surrogate-key'])
return response.headers['surrogate-key'];
}
});
} catch (error) {
logger.save(`${'(prod)'.padEnd(15)}error in retrieveSurrogateKey: ${error}`);
throw error
}
}
I know that the purgeRequestPromises executes early, because I get errors complaining that I've set my Surrogate-Key header as undefined in my HEAD request:
async requestPurgeOfSurrogateKey(surrogateKey) {
headers['Surrogate-Key'] = surrogateKey
try {
axios({
method: `POST`,
url: `https://api.fastly.com/service/${fastlyServiceId}/purge/${surrogateKey}`,
path: `/service/${fastlyServiceId}/purge${surrogateKey}`,
headers: headers,
})
.then(response => {
console.log("the status code for purging!! ", response.status)
if (response.status === 200) {
return true
}
});
} catch (error) {
logger.save(`${'(prod)'.padEnd(15)}error in requestPurgeOfSurrogateKey: ${error}`);
throw error;
}
}
retrieveSurrogateKey is synchronously returning undefined: the value in the try block is a promise and no errors are thrown synchronously, so the catch clause is never executed and execution falls out the bottom, returning undefined from the function body.
You could try something like:
function retrieveSurrogateKey(url) { // returns a promise
return axios({
// ^^^^^^
method: 'HEAD',
url: url,
headers: headers,
}).then(response => {
console.log("this is the response status: ", response.status)
if (response.status === 200) {
console.log("this is the surrogate key!! ", response.headers['surrogate-key'])
return response.headers['surrogate-key'];
}
}).catch(error => {
logger.save(`${'(prod)'.padEnd(15)}error in retrieveSurrogateKey: ${error}`);
throw error;
});
}
Note that it is superfluous to declare a function returning a promise as async if it doesn't use await. There is also a secondary problem in this line:
const surrogateKeyArray = await Promise.all(surrogateKeyPromises).catch(console.log);
The catch clause will will fulfill the promise chain unless the error is rethrown. You could (perhaps) leave off the .catch clause or recode it as
.catch( err=> { console.log(err); throw err} );
You don't have to remove async from retrieveSurrogateKey() in order for it to work. In fact it's more readable if you don't. As was already explained, the problem is that the promise returned by retrieveSurrogateKey() does not follow the completion of the promise returned by the call to axios(). You need to await it:
async retrieveSurrogateKey(url) {
try {
const response = await axios({
method: 'HEAD',
url,
headers,
});
console.log('this is the response status: ', response.status);
if (response.status === 200) {
const surrogateKey = response.headers['surrogate-key'];
console.log('this is the surrogate key!! ', surrogateKey);
return surrogateKey;
}
} catch (error) {
logger.save(`${'(prod)'.padEnd(15)}error in retrieveSurrogateKey: ${error}`);
throw error;
}
}
This preserves the same logic you currently have, but you'll notice that when response.status !== 200, you end up with a resolved promise of undefined, rather than a rejected promise. You might want to use validateStatus to assert the exact status of 200. By default axios resolves any response with a status >= 200 and < 300:
async retrieveSurrogateKey(url) {
try {
const response = await axios({
method: 'HEAD',
url,
headers,
validateStatus(status) {
return status === 200;
}
});
const surrogateKey = response.headers['surrogate-key'];
console.log('this is the surrogate key!! ', surrogateKey);
return surrogateKey;
} catch (error) {
logger.save(`${'(prod)'.padEnd(15)}error in retrieveSurrogateKey: ${error}`);
throw error;
}
}
This way, you're always guaranteed a surrogate key, or a rejected promise.

DELETE with Express.JS

I am trying to get my DELETE method to work. When the function is ran it is going straight into the throw Error line. In the console it is printing the following two errors. “404 Not Found” “Error Uncaught (in promise) Error"
Here is my client side code
async function deleteItem(item) {
let requestOptions = {
method: "DELETE",
headers: { "Content-Type": "application/json" },
}
const response = await fetch("/delete/:id", requestOptions);
if (response.status != 204) {
throw Error("Cannot delete your item from list");
}
return item;
}
And server side code
app.delete("/delete/:id"),
async (request, res) => {
try {
await Item.deleteOne({ _id: request.params.id });
res.sendStatus(204);
} catch {
res.sendStatus(404);
console.log('test');
}
};
You need to pass the ID of the "thing" that you want to delete in the client side code.
async function deleteItem(item) {
let requestOptions = {
method: "DELETE",
headers: { "Content-Type": "application/json" },
}
const response = await fetch("/delete/:id", requestOptions); // <----- HERE!
if (response.status != 204) {
throw Error("Cannot delete your item from list");
}
return item;
}
it should be something like (assuming that the item object has the id)
const response = await fetch(`/delete/${ item.id }`, requestOptions);
e.g.: /delete/12423
You aren't catching the promise returned from your fetch() function causing it to throw an error. Also, it looks like you aren't sending the request with a valid id.
You could fix that by doing
fetch(...).then((response) => {
if (response.status != 204) {
throw Error("Cannot delete your item from list");
}
}).catch((err) => {
console.error(err); // handle error
});
or
const response = await fetch(...).catch((err) => {
console.error(err); // handle error
});
// if the promise is rejected and the catch block is executed then 'response' will be undefined
if (!response) return;
if (response.status != 204) {
throw Error("Cannot delete your item from list");
}
edit: or of course you could not catch the promise rejection and just send the request with a valid id

How to resolve the converting circular structure to JSON issue in fetch

I want to get the list of the repositories by providing a username.
Below is what I have done so far.
router.get('/github/:username', (req, res) => {
try {
const url = `https://api.github.com/users/${req.params.username}/repos?per_page=5&sort=created:asc&client_id=${config.get('githubClientId')}&clientSecret=${config.get('githubSecret')}`;
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
};
console.log(url);
fetch(url, {
method: 'GET',
headers: headers,
}).then(data => {
if (data.status !== 200) {
return res.status(404).send({
msg: 'No GitHub profile found'
});
} else {
return data.json();
}
}).then(result => res.json(result));
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
})
When I use the dynamically created URL in the browser, I get the response
When I pass valid user-name, I get the repositories in Postman, where I am testing the API
When I pass invalid user-name, I get the following error
(node:18684) UnhandledPromiseRejectionWarning: TypeError: Converting circular structure to JSON
at JSON.stringify (<anonymous>)
at stringify (E:\Connector\node_modules\express\lib\response.js:1123:12)
at ServerResponse.json (E:\Connector\node_modules\express\lib\response.js:260:14)
at fetch.then.then.result (E:\Connector\routes\api\profile.js:396:31)
at process._tickCallback (internal/process/next_tick.js:68:7)
(node:18684) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)
Can anybody tell me how to resolve this error? I have looked at many resources but could not find any concrete.
The problem is the return res.status(404).send(…) in the first then callback. The second then callback will then try to res.json(result) that return value.
You should instead write
router.get('/github/:username', (req, res) => {
const url = `https://api.github.com/users/${req.params.username}/repos?per_page=5&sort=created:asc&client_id=${config.get('githubClientId')}&clientSecret=${config.get('githubSecret')}`;
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
};
console.log(url);
fetch(url, {
method: 'GET',
headers: headers,
}).then(data => {
if (data.status !== 200) {
res.status(404).send({
msg: 'No GitHub profile found'
});
} else {
return data.json().then(result => {
res.json(result);
});
}
}).catch(err => {
console.error(err.message);
res.status(500).send('Server Error');
});
})

Firebase JS http function is returning the incorrect response received from an axios API GET call

I have written the following HTTP firebase JS function which is returning the incorrect status 500 error response using Postman even though the axios GET call response from the API service has returned the correct 200 status response (confirmed by the console output screenshot below)
exports.doshiiMenuUpdatedWebhook = functions.https.onRequest((req, res) => {
if (req.method === 'PUT') {
return res.status(403).send('Forbidden!');
}
return cors(req, res, () => {
let verify = req.query.verify;
if (!verify) {
verify = req.body.verify;
}
let locationId = req.body.data.locationId
let posId = req.body.data.posId
let type = req.body.data.type
let uri = req.body.data.uri
let itemUri = req.body.data.itemUri
console.log('locationId', locationId);
console.log('posId', posId);
console.log('type', type);
console.log('uri', uri);
console.log('itemUri', itemUri);
const options = {
headers: {'authorization': 'Bearer ' + req.query.verify}
};
return axios.get(uri, options)
.then(response => {
console.log('response data: ', response.data);
console.log('response status: ', response.status);
console.log('response statusText: ', response.statusText);
console.log('response headers: ', response.headers);
console.log('response config: ', response.config);
return res.status(200).json({
message: response
})
})
.catch(err => {
return res.status(500).json({
error: err
})
});
});
});
In Postman I'm expecting to see "Status: 200" response, but I get this:
There is no error report in the Firebase console other than this:
As explained in the Express documentation:
res.json([body])
Sends a JSON response. This method sends a response (with the correct
content-type) that is the parameter converted to a JSON string using
JSON.stringify().
The parameter can be any JSON type, including object, array, string,
Boolean, number, or null, and you can also use it to convert other
values to JSON.
Following the "debugging" we did through the comments/chat, it seems that the
{message: response}
object that you pass to json() generates the error.
Following the HTTP Cloud Functions documentation, which states:
Important: Make sure that all HTTP functions terminate properly. By
terminating functions correctly, you can avoid excessive charges from
functions that run for too long. Terminate HTTP functions with
res.redirect(), res.send(), or res.end().
and since you explained in the chat that you "only need to return the status code" and that you "want to save the json data to: admin.database().ref(/venue-menus/${locationId}/menu)",
I would advise you do as follows:
exports.doshiiMenuUpdatedWebhook = functions.https.onRequest((req, res) => {
if (req.method === 'PUT') {
return res.status(403).send('Forbidden!');
}
cors(req, res, () => {
let verify = req.query.verify;
if (!verify) {
verify = req.body.verify;
}
let locationId = req.body.data.locationId
let posId = req.body.data.posId
let type = req.body.data.type
let uri = req.body.data.uri
let itemUri = req.body.data.itemUri
const options = {
headers: { 'authorization': 'Bearer ' + req.query.verify }
};
axios.get(uri, options)
.then(response => {
console.log('response data: ', response.data);
return admin.database().ref(`/venue-menus/${locationId}/menu`).set(response.data)
})
.then(response => {
return res.status(200).end()
})
.catch(err => {
return res.status(500).send({
error: err
})
})
})
});

How to get HTTP error detail and text from fetch and Promises?

A lot of posts, questions and articles explain how to handle HTTP errors from fetch requests, but solutions only point logging or just use text of the answer, but i'd like to get response details (status + statusText) and response text (given by the server)
I want to have a generic fetch_api that I could use in my different components then. For the valid response it's working, but for the error I can't manage to get details + text from server, because I need to return a rejected Promise but I can't make it working, here are my tries to get
response.status
response.statusText
response.url
response.text() // this is a Promise and I can't get it and also the details
static fetch_api(url, method = 'get') {
return fetch(url, {method: method})
.then(res => {
if (res.ok) return res.json();
throw res;
})
.then((result) => result
, (error) => {
const err = ("[" + error.status + ":" + error.statusText + "] while fetching " +
error.url + ", response is " + error.text()) // how to get server text
return Promise.reject(err) // ok but server text missing
//Er [404:NOT FOUND] while ... is [object Promise]
return Promise.reject().then(() => error.text()) // Not ok
return Promise.reject(error.status) // OK but need all
return Promise.reject(error.statusText) // OK but need all
}
)
}
// USE
fetchStatus() {
App.fetch_get(host + "/statusERROR")
.then(result => this.setState({isLoaded: true, modules: result}))
.catch(error => this.setState({isLoaded: true, error: {message: error}}))
}
Sources for basic handling, mots of them is just logging
react native fetch http request throw error
Fetch image in React from Node.js request
https://github.com/github/fetch/issues/203
https://gist.github.com/odewahn/5a5eeb23279eed6a80d7798fdb47fe91
https://www.tjvantoll.com/2015/09/13/fetch-and-errors/
https://gist.github.com/philipszdavido/fea2e353805294f374b6cb121ce43fa5
Finally got it : use the res.text() promise, and return a Promise.reject() inside of i
static fetch_api(url, method = 'get') {
return fetch(url, {method: method})
.then(res => {
if (res.ok) return res.json();
throw res;
})
.then(result => result)
.catch(error => error.text().then(errormsg => Promise.reject("[" + error.status + ":" +
error.statusText + "] while fetching " + error.url + ", response is [" + errormsg+"]"))
)
}
To be used like
App.fetch_get(host + "/status")
.then(result => this.setState({items: result}))
.catch(error => this.setState({error: error}}))

Categories