I am writing an API endpoint which searches Google Book's API to add book titles to the notebooks in my MongoDB Database based on their Id's. I have it working properly, but sometimes my endpoint's response array returns missing the last item. My console logs seem to indicate my endpoint response is being sent before the Https request has finished fetching all the data. I need to solve the endpoint sometimes returning an incomplete array. I think I may need to include an async await somewhere but I'm not sure where. Any help is much appreciated!
Here is my code:
//GET a user's books with notes
const getNotebooks = async (req, res) => {
...
const books = await Book.find({ userId }).sort({createdAt: -1}) //gets all notebooks as array
let results = []
for (let i = 0; i < books.length; i++){
console.log(i + " - " + books[i].gId)
const item = {
id: books[i].userId,
gId: books[i].gId,
title: '',
}
const request = https.request(apiUrl + books[i].gId, async (response) => {
let data = '';
response.on('data', (chunk) => {
data = data + chunk.toString();
});
response.on('end', () => {
const body = JSON.parse(data);
item.title = body.volumeInfo.title
results.push(item)
if (i == books.length - 1){
res.status(200).json(results)
}
});
})
request.on('error', (error) => {
console.log('An error', error);
res.status(400).json({error: error})
});
request.end()
}}
You can use async-await with Promises only, since Node's core https module does not have any build-in promise support, you will have to convert it first into the promise format, then you can use async-await with it, i am not familiar with the standard https module but i will make an example with your code
const getNotebooks = async (req, res) => {
//...
const books = await Book.find({ userId }).sort({createdAt: -1}) //gets all notebooks as array
let results = []
for (let i = 0; i < books.length; i++){
console.log(i + " - " + books[i].gId)
const item = {
id: books[i].userId,
gId: books[i].gId,
title: '',
}
const options = apiUrl + books[i].gId
// now you can use await to this function which return a promise
await makeRequest(options, res, item, results, books )
}
}
// Making this function into a promise outside of the scope from your getNotebooks function.
function makeRequest(options, res, item, results, books) {
return new Promise((resolve, rejects) => {
const request = https.request(options, (response) => {
let data = '';
response.on('data', (chunk) => {
data = data + chunk.toString();
});
response.on('end', () => {
const body = JSON.parse(data);
resolve(body);
item.title = body.volumeInfo.title
results.push(item)
if (i == books.length - 1){
res.status(200).json(results)
}
});
});
request.on('error', (error) => {
console.log('An error', error);
rejects(error);
res.status(400).json({error: error})
});
request.end()
});
};
Alternatively you can also use a more modern way to request data, for example with fetch which is now available in node, or even Axios library, if you don't want any dependency, fetch should be the way to go, an example if there is no mistake and assuming is a GET method request:
const getNotebooks = async (req, res) => {
//...
try {
const books = await Book.find({ userId }).sort({createdAt: -1}) //gets all notebooks as array
let results = []
for (let i = 0; i < books.length; i++) {
console.log(i + " - " + books[i].gId)
const item = {
id: books[i].userId,
gId: books[i].gId,
title: '',
}
// using fetch on node js is now possible without any package to install
const response = await fetch(apiUrl + books[i].gId)
const data = await response.json();
item.title = data.volumeInfo.title
results.push(item)
if (i == books.length - 1){
res.status(200).json(results)
}
}
} catch (error) {
res.status(400).json({error: error})
};
};
As you can see you would write 2 line code instead of 20+ line.
Related
So, my problem is when I try to login in my vue app, the backend automatically stops when I try to fetch from it an array of objects.
To be more specific.
This is my fetch "attempt" to retrieve the objects from the database.
let url = utils.url;
let requestParam = utils.globalRequestParameters;
requestParam.method = "GET";
requestParam.body = null;
if (cars.value.length == 0) {
fetch(url + "cars", requestParam).then((res) =>
res.json().then(async (res) => {
store.dispatch("Car/fetchCars", res);
fetch(url + "users", requestParam).then((users) =>
users.json().then((users) => {
for (let car of res) {
let userCar = Object.values(users).find(
(a) => a.id == car.userId
);
car.userName = userCar.lastName + " " + userCar.firstName;
}
})
);
})
);
}
And login in view Login.vue
let requestParameters = utils.globalRequestParameters;
requestParameters.method = "POST";
requestParameters.body = JSON.stringify(data);
fetch(utils.url + "login", requestParameters).then((res) => {
res.json().then((res) => {
this.mesaj = res.message;
console.log("token:" + res.token);
if (res.token) {
localStorage.setItem("token", res.token);
console.log("token:" + res.token);
console.log("id:" + res.id);
let id = res.id;
requestParameters.method = "GET";
requestParameters.body = null;
this.$store.dispatch("login", true);
fetch(utils.url + "users/" + id, requestParameters).then(
(res) => {
res.json().then((res) => {
console.log("Jos de tot");
this.$store.dispatch("User/setUser", res);
console.log(this.$store.state.User.user);
this.$router.push("/");
});
}
);
}
});
});
}
},
Note.
cars is a computed value that return store.state.cars
and utils is
let url = "http://127.0.0.1:3000/";
let globalRequestParameters = {
method: "GET",
mode: "cors",
cache: "no-cache",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
},
redirect: "follow",
referrerPolicy: "no-referrer",
};
module.exports.globalRequestParameters = globalRequestParameters;
module.exports.url = url;
Here at the first fetch the backend stops and also the fetch it is not done.
And the backend route is
router.get('/cars', async (req, res) => {
res.json(await functions.getAllCars(req,res));
})
getAllCars = async (req, res) => {
const snapshot = await db.collection("Cars").get();
let cars = [];
snapshot.forEach((doc) => {
let car = {
id: doc.id,
userId: doc.data().userId,
manufacturer: doc.data().manufacturer,
model: doc.data().model,
color: doc.data().color,
plate: doc.data().plate,
price: doc.data().price,
description: doc.data().description
};
cars.push(car);
});
res.status(200).send(cars);
return
};
router.get("/users/:id", async (req, res) => {
res.json(await functions.getUserById(req.params.id, res));
});
getUserById = (id, res) => {
db
.collection("Users")
.doc(id)
.get()
.then((response) => {
let user = {};
user.id = response.id;
user.firstName = response.data().firstName;
user.lastName = response.data().lastName;
user.gender = response.data().gender;
user.jobTitle = response.data().jobTitle;
user.phone = response.data().phone;
user.email = response.data().email;
user.isAdmin = response.data().isAdmin;
res.status(200).send(user);
return
})
.catch((err) => {
res.status(404).send({ message: "User not found" });
return
});
};
The user is retrieved correctly, I see it in console through a console log, but the messages that I get in the terminal and console are:
*As a final note. I use vue 3, node.js version 16.13.0 and Firestore as Database. And yesterday everything was working perfectly fine on my other computer but I had to go somewhere and use my laptop. Maybe there is something about my laptop. All I did was just to install the modules for the front and back
I think this has nothing to do with Vue - it is simply the problem of your Express backend code
ERR_HTTP_HEADERS_SENT: Cannot set headers after they are sent to the client
As described here:
That particular error occurs whenever you try to send more than one response to the same request and is usually caused by improper asynchronous code.
getAllCars
getAllCars is async function with await inside - as soon as this await is hit (together with db.collection("Cars").get() call), function returns Promise which is awaited at res.json(await functions.getAllCars(req,res));
When the DB call finishes, the rest of the method is executed including res.status(200).send(cars) - this will send the cars array to the client and returns undefined (this is what simple return does) and res.json(undefined) is executed causing the error above (you are trying to send second response)
getUserById
You say that this handler works fine but I really doubt it - from what I see, this should NOT work either
You are calling it with res.json(await functions.getUserById(req.params.id, res));. To await actually doing something, the awaited function must return a Promise (either implicitly by using await inside or explicitly) or general "thenable" object. The getUserById function returns nothing (return statements inside then() or catch() does not count! ...those are different functions)
This problem can be fixed by doing return db.collection("Users").doc(id).get().then() but then you will get same error as in getAllCars case
Correct pattern
Do not use res.status(200).send() and res.json() together
For the sake of sanity (at least until you really know what you are doing) do not mix promises with async/await
async functions should return the data (do not use return without "argument")
Following code shows both Promise based and async/await style (it "pseudo code" in the sense I did not tested it but hopefully you get the idea)
router.get('/cars', async (req, res) => {
try {
const response = await functions.getAllCars()
res.status(200).json(response);
} catch() {
res.sendStatus(500)
}
})
getAllCars = async () => {
const snapshot = await db.collection("Cars").get();
let cars = [];
snapshot.forEach((doc) => {
let car = {
id: doc.id,
userId: doc.data().userId,
manufacturer: doc.data().manufacturer,
model: doc.data().model,
color: doc.data().color,
plate: doc.data().plate,
price: doc.data().price,
description: doc.data().description
};
cars.push(car);
});
// res.status(200).send(cars); //* should be handled by caller
return cars //* return the data
};
router.get("/users/:id", async (req, res) => {
functions.getUserById(req.params.id)
.then((response) => {
if(response === null)
res.status(404).json({ message: "User not found" });
else
res.status(200).json(response);
})
.catch(er) {
res.status(500).send(er.message)
}
});
getUserById = (id) => {
return db //* return the promise
.collection("Users")
.doc(id)
.get()
.then((response) => {
let user = {};
user.id = response.id;
user.firstName = response.data().firstName;
user.lastName = response.data().lastName;
user.gender = response.data().gender;
user.jobTitle = response.data().jobTitle;
user.phone = response.data().phone;
user.email = response.data().email;
user.isAdmin = response.data().isAdmin;
// res.status(200).send(user); //* should be handled by caller
return user //* return the data
})
.catch((err) => {
return null
});
};
I am new to using cloud functions for Firebase with an http triggered function and I am confused on how to properly terminate the function. I'm not sure if I should be using res.sendStatus, returning a promise, or both.
The goal of my function is to loop through several documents in the collection 'communities'. Each community has a collection of documents where I query the document with the highest value of 'hotScore'. I then send an iOS push notification containing that document to a topic (all users in that given community).
Unfortunately, I am getting several errors when the code is run such as Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client and Unhandled rejection. I'm pretty such this is due to my negligence in handling function termination, although I have been confused by the online resources I have looked at so far. Would someone mind taking a look at my code/pointing me in the right direction? Thank you so much!
exports.sendNotificationTrendingPost = functions.https.onRequest(async (req, res) => {
//Get communities collection from Firestore
return admin.firestore().collection('communities').get().then((communities) => {
var communityPromises = [];
//Loop through each community
communities.forEach((community) => {
let communityID = community.get('communityID');
let communityName = community.get('name');
//Get the post with the highest hotScore
let communityPromise = admin.firestore().collection('communities').doc(communityID).collection('posts').orderBy('hotScore', 'desc').limit(1).get().then((posts) => {
let hottestPost = posts[0];
let postID = hottestPost.get('postID');
let postText = hottestPost.get('text');
let currentDate = Date.now() / 1000;
var message;
//Verify that the hottest post was posted in the past 24 hours
if (hottestPost.get('date') > (currentDate - 86400)) {
//Build the notification text (shortening if too long)
let shortenedPostText = postText.substring(0,60);
var textEnd = '';
if (postText.length > 60) {
textEnd = '...';
}
let notificationText = 'Trending post on ' + communityName + ': ' + shortenedPostText + textEnd;
//Build the push notification
message = {
apns: {
headers: {
'apns-push-type': 'alert'
},
payload: {
aps: {
alert: {
body: notificationText,
},
},
postID: postID,
},
},
topic: communityID
}
}
//Send the message and return the promise
if (message === null) {
return null;
} else {
return admin.messaging().send(message);
}
})
.catch(error => {
console.log(error);
res.status(500).send(error);
})
if (communityPromise !== null) {
communityPromises.push(communityPromise);
}
})
res.sendStatus(200);
return Promise.all(communityPromises);
})
.catch(error => {
console.log(error);
res.status(500).send(error);
})
})
As samthecodingman advised, it is much better to use async/await in your case, as it will simplify the code and will make it much easier to read.
The following changes should do the trick (untested). Note how we use an Array of Community names in order to pass the names from one loop to the other. This works because, with Promise.all(), the returned values are in order of the Promises passed, regardless of completion order.
exports.sendNotificationTrendingPost = functions.https.onRequest(async (req, res) => {
try {
const db = admin.firestore();
const communitiesQuerySnap = await db.collection('communities').get();
const communityPromises = [];
const communityNames = [];
communitiesQuerySnap.forEach((community) => {
let communityID = community.get('communityID');
let communityName = community.get('name');
communityNames.push(communityName);
communityPromises.push(db.collection('communities').doc(communityID).collection('posts').orderBy('hotScore', 'desc').limit(1).get())
});
const postsQuerySnapArray = await Promise.all(communityPromises);
const messagePromises = [];
postsQuerySnapArray.forEach((postsQuerySnap, index) => {
const hottestPost = postsQuerySnap.docs[0];
const postID = hottestPost.get('postID');
const postText = hottestPost.get('text');
const currentDate = Date.now() / 1000;
let message;
if (hottestPost.get('date') > (currentDate - 86400)) {
//Build the notification text (shortening if too long)
let shortenedPostText = postText.substring(0, 60);
var textEnd = '';
if (postText.length > 60) {
textEnd = '...';
}
const communityName = communityNames[index]; // The two Arrays postsQuerySnapArray and communityName have the same order, because Promise.all keeps the order.
let notificationText = 'Trending post on ' + communityName + ': ' + shortenedPostText + textEnd;
//Build the push notification
message = {
apns: {
headers: {
'apns-push-type': 'alert'
},
payload: {
aps: {
alert: {
body: notificationText,
},
},
postID: postID,
},
},
topic: communityID
}
messagePromises.push(admin.messaging().send(message));
}
})
await Promise.all(messagePromises);
res.status(200).send({ result: "completed" }); // Or res.end()
} catch (error) {
console.log(error);
res.status(500).send(error);
}
});
I'm new to Node.js and I'm creating a simple pagination page. The REST API works fine, but consuming it has left me in limbo.
Here is the REST API (other parts have been taken out for brevity)
const data = req.query.pageNo;
const pageNo =
(typeof data === 'undefined' || data < 1) ? 1 : parseInt(req.query.pageNo);
let query = {};
const total = 10;
query.skip = (total * pageNo) - total;
query.limit = total;
try {
const totalCount = await Users.countDocuments();
const pageTotal = Math.ceil(totalCount / total);
const users = await Users.find({}, {}, query);
return res.status(200).json(users);
} catch (error) {
console.log('Error ', error);
return res.status(400).send(error)
};
};
When I return the json with just the 'users' object, like so return res.status(200).json(users); the page renders correctly, but when I pass in other objects like what I have in the code, it fails. This is how I'm consuming the API:
const renderHomepage = (req, res, responseBody) => {
let message = null;
if (!(responseBody instanceof Array)) {
message = 'API lookup error';
responseBody = [];
} else {
if (!responseBody.length) {
message = 'No users found nearby';
}
}
res.render('users-list', {
title: 'Home Page',
users: responseBody,
message: message
});
}
const homelist = (req, res) => {
const path = '/api/users';
const requestOptions = {
url: `${apiOptions.server}${path}`,
method: 'GET',
json: true,
};
request(
requestOptions,
(err, {statusCode}, body) => {
if (err) {
console.log('Ther was an error ', err);
} else if (statusCode === 200 && body.length) {
renderHomepage(req, res, body);
} else if (statusCode !== 200 && !body.length) {
console.log('error ',statusCode);
}
}
);
}
I've searched extensively on both here and other resources but none of the solutions quite answers my question. I hope someone could be of help
I found quite unusual behavior of Promise.all().finally() - look like it returning data before map() was applied to this.
1. Data received from database.
2. Making call to Google Maps API inside map(), applied to the data retrieved from database and adding to the object property "Distance" with result from Google API call.
3. Return data in Promise.all() - data received without new property.
I can't get how this even possible?
public static get = async (req: Request, res: Response) => {
const latitude = req.query.lat;
const longitude = req.query.long;
const pool = await new sql.ConnectionPool(CommonConstants.connectionString).connect();
const request = pool.request();
const result = await request.execute('SuppliersSP');
sql.close();
const rows = result.recordset.map(async (supplier) => {
const data = { origin: [latitude, longitude], destination: [supplier.Latitude, supplier.Longitude] };
const distance = await GetDistance(data) || 0;
Object.defineProperty(supplier, 'Distance', {
enumerable: true,
configurable: true,
writable: true,
value: distance
});
return supplier;
})
Promise.all(rows).finally(() => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.status(200).json(rows);
});
}
Here is GetDistance function:
import { Constants } from "./constants";
const https = require('https');
export function GetDistance(coords) {
const { origin, destination } = coords;
return new Promise((resolve, reject) => {
https.get(`${Constants.GoogleMapsUrl}?origins=${origin[0]},${origin[1]}
&destinations=${destination[0]},${destination[1]}
&key=${Constants.GoogleMapsApiKey}`, (resp) => {
let data = '';
resp.on('data', (chunk) => {
data += chunk;
});
resp.on('end', () => {
const distance = JSON.parse(data);
resolve(distance.rows[0].elements[0].distance.value);
});
}).on("error", (err) => {
reject("Error: " + err.message);
});
});
}
It was solved by a change .finally() to .then().
I have a koa router I need to call a api where will async return result. This means I cannot get my result immediately, the api will call my callback url when it's ok. But now I have to use it like a sync api which means I have to wait until the callback url is called.
My router like this:
router.post("/voice", async (ctx, next) => {
// call a API here
const params = {
data: "xxx",
callback_url: "http//myhost/ret_callback",
};
const req = new Request("http://xxx/api", {
method: "POST",
body: JSON.stringify(params),
});
const resp = await fetch(req);
const data = await resp.json();
// data here is not the result I want, this api just return a task id, this api will call my url back
const taskid = data.taskid;
// now I want to wait here until I got "ret_callback"
// .... wait .... wait
// "ret_callback" is called now
// get the answer in "ret_callback"
ctx.body = {
result: "ret_callback result here",
}
})
my callback url like this:
router.post("/ret_callback", async (ctx, next) => {
const params = ctx.request.body;
// taskid will tell me this answer to which question
const taskid = params.taskid;
// this is exactly what I want
const result = params.text;
ctx.body = {
code: 0,
message: "success",
};
})
So how can I make this aync api act like a sync api?
Just pass a resolve() to another function. For example, you can do it this way:
// use a map to save a lot of resolve()
const taskMap = new Map();
router.post("/voice", async (ctx, next) => {
// call a API here
const params = {
data: "xxx",
callback_url: "http//myhost/ret_callback",
};
const req = new Request("http://xxx/api", {
method: "POST",
body: JSON.stringify(params),
});
const resp = await fetch(req);
const data = await resp.json();
const result = await waitForCallback(data.taskid);
ctx.body = {
result,
} })
const waitForCallback = (taskId) => {
return new Promise((resolve, reject) => {
const task = {};
task.id = taskId;
task.onComplete = (data) => {
resolve(data);
};
task.onError = () => {
reject();
};
taskMap.set(task.id, task);
});
};
router.post("/ret_callback", async (ctx, next) => {
const params = ctx.request.body;
// taskid will tell me this answer to which question
const taskid = params.taskid;
// this is exactly what I want
const result = params.text;
// here you continue the waiting response
taskMap.get(taskid).onComplete(result);
// not forget to clean rubbish
taskMap.delete(taskid);
ctx.body = {
code: 0,
message: "success",
}; })
I didn't test it but I think it will work.
function getMovieTitles(substr) {
let movies = [];
let fdata = (page, search, totalPage) => {
let mpath = {
host: "jsonmock.hackerrank.com",
path: "/api/movies/search/?Title=" + search + "&page=" + page,
};
let raw = '';
https.get(mpath, (res) => {
res.on("data", (chunk) => {
raw += chunk;
});
res.on("end", () => {
tdata = JSON.parse(raw);
t = tdata;
totalPage(t);
});
});
}
fdata(1, substr, (t) => {
i = 1;
mdata = [];
for (i = 1; i <= parseInt(t.total_pages); i++) {
fdata(i, substr, (t) => {
t.data.forEach((v, index, arrs) => {
movies.push(v.Title);
if (index === arrs.length - 1) {
movies.sort();
if (parseInt(t.page) === parseInt(t.total_pages)) {
movies.forEach(v => {
console.log(v)
})
}
}
});
});
}
});
}
getMovieTitles("tom")
Okay so first of all, this should not be a "goal" for you. NodeJS works better as ASync.
However, let us assume that you still want it for some reason, so take a look at sync-request package on npm (there is a huge note on there that you should not this in production.
But, I hope you mean on how to make this API simpler (as in one call kinda thingy). You still need .next or await but it will be be one call anyway.
If that is the case, please comment on this answer I can write you a possible method I use myself.
How about this ?
router.post("/voice", async (ctx, next) => {
const params = {
data: "xxx",
callback_url: "http//myhost/ret_callback",
};
const req = new Request("http://xxx/api", {
method: "POST",
body: JSON.stringify(params),
});
const resp = await fetch(req);
const data = await resp.json();
// data here is not the result I want, this api just return a task id, this api will call my url back
const taskid = data.taskid;
let response = null;
try{
response = await new Promise((resolve,reject)=>{
//call your ret_callback and when it finish call resolve(with response) and if it fails, just reject(with error);
});
}catch(err){
//errors
}
// get the answer in "ret_callback"
ctx.body = {
result: "ret_callback result here",
}
});