Firebase function promise undefined TypeError - javascript

I am currently using the firebase functions to call the following doc from the database:
let token, userId;
db.doc(`/users/${newAccount.username}`)
.get()
.then((doc) => {
if (doc.exists === false) {
return firebase.auth().createUserWithEmailAndPassword(newAccount.email, newAccount.password).catch(err => console.error(err));
} else {
res.status(400).json({ username: 'this username is already taken' });
}
})
.then(data => {
userId = data.user.uid;
return data.user.getIdToken();
})
.then((idToken) => {
token = idToken;
const userCredentials = {
username: newAccount.username,
email: newAccount.email,
created: new Date().toISOString(),
userId
};
return db.doc(`/users/${newAccount.username}`).set(userCredentials);
})
.then(() => {
return res.status(201).json({ token });
})
.catch((err) => {
console.error(err);
if (err.code === 'auth/email-already-in-use') {
return res.status(400).json({ email: 'Email is already is use' });
} else {
return res.status(500).json({ general: 'Something went wrong, please try again' });
}
});
The code runs fine but there is a logged error, if the doc exists in the database:
TypeError: Cannot read property 'user' of undefined
I presume the promise is still running and I am a bit stuck on how to end it?
Any help would be grateful. Thanks.

Your second then callback is going to get invoked in all situations. Sending the 400 response in the first callback isn't actually going to stop the promise from propagating to all of the following then callbacks.
If you want to stop the chain of then callbacks from executing, you should instead throw an error to get picked up by a catch down the chain, skipping all the then.

Related

Javascript: using catch block but not to handle an error

I'm in a situation where I have to use a catch block to execute some code but I don't want to consider it an error.
Basically, I want to update/create a user based on whether the user is already registered or not respectively. The admin sdk let me create a user, and if the user already exists it throws an error. So if I'm in the catch block I know that the user already exists and I want to update it.
function addClient(client) {
return new Promise((resolve, reject) => {
admin.auth().createUser({
uid: client.id,
email: client.email,
emailVerified: true,
password: client.password,
}).then(record => {
resolve(record);
return null;
}).catch(
// the user already exist, I update it
admin.auth().updateUser(client.id, {
email: client.email
}).then(record => {
resolve(record);
return null;
}).catch(
err => {
reject(err);
}
)
);
});
}
The problem is that when I call the function with an existing user, it is updated correctly but the HTTP response is an internal server error (I guess because it enters the catch block and it considers this as an error). The same is if I send a new user: it is created correctly but the HTTP response code is a 500.
There is a way to avoid this behaviour?
This is the main function that calls the previous one for each user received and it's responsible for sending the HTTP response:
exports.addClients = functions.https.onRequest((req, res) => {
// fetch recevied list from payload
var receivedClients = req.body.clients;
var promises = [];
receivedClients.forEach(client => {
promises.push(addClient(client));
})
Promise.all(promises)
.then(() => {
res.sendStatus(200);
return null;
})
.catch(err => {
res.status(500).send(err);
});
});
I guess that what I want to achieve is to have all the promises resolving.
You need to pass a callback to .catch, not a promise. Also avoid the Promise constructor antipattern!
function addClient(client) {
return admin.auth().createUser({
uid: client.id,
email: client.email,
emailVerified: true,
password: client.password,
}).catch(err => {
// ^^^^^^^^
// if (err.code != "UserExists") throw err;
return admin.auth().updateUser(client.id, {
email: client.email
})
});
}

How do you solve the "Cannot set headers after they are sent to the client" error in node js?

I've rewritten the following function about 6 different times and am still getting a "Cannot set headers after they are sent to the client" error. I have found several posts on the topic of promises but still cant figure it out:
Error: Can't set headers after they are sent to the client
Cannot set headers after they are sent to the client
Error: Setting header after it is sent - Help me understand why?
The following function is for a forum and is triggered when a comment is submitted. It check to see that the forum post exists, than if a parent comment exists (in the case it is a subcomment). I am using firestore.
index.js
const functions = require('firebase-functions');
const app = require('express')();
const {postOneForumComment,
} = require('./handlers/forumPosts');
app.post('/forumPost/:forumPostId/:parentId/comment', FBAuth, postOneForumComment);
exports.api = functions.https.onRequest(app);
forumPosts.js
// submit a new comment
exports.postOneForumComment = (req, res) => {
if (req.body.body.trim() === '')
return res.status(400).json({ comment: 'Must not be empty' });
const newComment = {
body: req.body.body,
forumPostId: req.params.forumPostId,
parentId: req.params.parentId
};
db.doc(`/forumPosts/${req.params.forumPostId}`) //check to see if the post exists
.get()
.then((doc) => {
if (!doc.exists) {
return res.status(404).json({ error: 'Post not found' });
}
else if (req.params.forumPostId !== req.params.parentId) { //check to see if the comment is a subcomment
return db.doc(`/forumComments/${req.params.parentId}`) //check to see if the parent comment exists
.get();
}
return "TopLevelComment";
})
.then((data) => {
if (data === 'TopLevelComment' || data.exists) {
return db.collection('forumComments').add(newComment); //post the comment to the database
}
return res.status(500).json({ error: 'Comment not found' });
})
.then(() => {
res.json(newComment);
})
.catch((err) => {
console.log(err.message);
res.status(500).json({ error: 'somethign went wrong' });
});
};
ERROR:
(node:29820) 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: 1)
(node:29820) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with
a non-zero exit code.
There are two ways of using promises. Either you use the then/catch callbacks or you can use async/await to allow you to write them synchronously.
then/catch method
// Some code before promise
somePromise.then(() => {
// Some code after promise action is successful
}).catch(err => {
// Some code if promise action failed
})
// Some code after promise definition you think should run after the above code
// THIS IS WHAT IS HAPPENING WITH YOUR CODE
async/await method
// Some code before promise
await somePromise;
// Some code after promise action is successful
The latter approach was introduces to avoid the callback hell problem and it seems that's where your error is arising from.
When using callback callbacks you must make sure that nothing is defined after the promise definition else it will run before the promise resolves (Which is counter-intuitive since placing code B after code B should make A run before B)
Your error is because your callbacks are probably running AFTER the response has been sent and express does not allow you to send multiple responses for a request.
You should make sure that where ever res.send or res.json is being called exist within the callback.
This article should help you understand promises much better...
Hope this helps...
For anyone who stumbles upon this here is a working solution using Promise.all to make sure all promises are fulfilled before moving on. It is not the prettiest function and I plan on going back and turning it into an async/await ordeal per #kwame and #Ajay's recommendation... but for now it works.
// post a comment
// TODO: turn into async await function
exports.postOneForumComment = (req, res) => {
if (req.body.body.trim() === '') return res.status(400).json({ comment: 'Must not be empty' });
const newComment = {
body: req.body.body,
createdAt: new Date().toISOString(),
forumPostId: req.params.forumPostId,
parentId: req.params.parentId,
username: req.user.username,
userImage: req.user.imageUrl,
likeCount: 0
};
const parentPost =
db.doc(`/forumPosts/${req.params.forumPostId}`).get()
.then((doc) => {
if (!doc.exists) {
res.status(404).json({ error: 'Post not found' });
return false;
}
return true;
})
.catch((err) => {res.status(500).json({ error: 'something went wrong while checking the post' });});
const parentComment =
req.params.forumPostId === req.params.parentId ? true :
db.doc(`/forumComments/${req.params.parentId}`).get()
.then((doc) => {
if (!doc.exists) {
res.status(404).json({ error: 'Comment not found' });
return false;
}
if (doc.forumPostId !== req.params.forumPostId) {
res.status(404).json({ error: 'Comment is not affiliated with this post' });
return false;
}
return true;
})
.catch((err) => {res.status(500).json({ error: 'something went wrong while checking the comment' });});
Promise.all([parentPost, parentComment])
.then((values) => {
if (values[0] && values[1]) {
return db.collection('forumComments')
.add(newComment)
.then(() => {
res.json(newComment);
});
}
return console.log("there was an error");
})
.catch((err) => {
res.status(500).json({ error: 'somethign went wrong with the submission' });
});
};

Chaining more than one promise function in Node.js for mongoDB

There is a requirement, that review can be added only when product and user both are found. So i have written code in order to implement this scenario.
User.findById(req.params.userId).exec()
.then(response => {
console.log("response", response);
if (response) {
return Product.findById(req.params.productId).exec();
}
else {
return res.status(404).json({
message: 'User not found'
})
}
})
.then(response => {
console.log("response", response);
if (!response) {
return res.status(404).json({
message: 'Product not found'
})
}
const review = new Review({
_id: mongoose.Types.ObjectId(),
rev: req.body.rev,
productId: req.params.productId,
userId: req.params.userId
});
return review.save();
})
.then(response => {
console.log("responseeeeeeee", response);
res.status(201).json({
response
})
})
.catch(error => {
console.log("error", error);
res.status(500).json({
error
})
})
This is working fine, but as soon as product or user is missing it is throwing a warning as such :
(node:17276) UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (_http_outgoing.js:470:11)
at ServerResponse.header (D:\backup-learning\project-shop-always\node_modules\express\lib\response.js:771:10)
at ServerResponse.send (D:\backup-learning\project-shop-always\node_modules\express\lib\response.js:170:12)
at ServerResponse.json (D:\backup-learning\project-shop-always\node_modules\express\lib\response.js:267:15)
at User.findById.exec.then.then.then.catch.error (D:\backup-learning\project-shop-always\api\controllers\review-controller.js:58:29)
at process._tickCallback (internal/process/next_tick.js:68:7)
The problem is that the chain continues with the return value of res.status as the fulfillment value when you do return res.status(/*...*/) from within the then callback.
You can't use a single chain for this. Also, since you need to locate both the user and the product, probably best to do that bit in parallel. See comments:
// *** Start both the product and user search in parallel
Promise.all([
User.findById(req.params.userId).exec(),
Product.findById(req.params.productId).exec()
])
.then(([user, product]) => {
// *** Handle it if either isn't found
if (!user) {
res.status(404).json({
message: 'User not found'
});
} else if (!product) {
res.status(404).json({
message: 'Product not found'
});
} else {
// *** Both found, do the review
const review = new Review({
_id: mongoose.Types.ObjectId(),
rev: req.body.rev,
productId: req.params.productId,
userId: req.params.userId
});
// *** Return the result of the save operation
return review.save()
.then(response => {
console.log("responseeeeeeee", response);
res.status(201).json({
response
});
}
}
// *** Implicit return of `undefined` here fulfills the promise with `undefined`, which is fine
})
.catch(error => {
// *** An error occurred finding the user, the product, or saving the review
console.log("error", error);
res.status(500).json({
error
})
});
If you're doing this in any modern version of Node.js, you can use an async function and await:
// *** In an `async` function
try {
const [user, product] = await Promise.all([
User.findById(req.params.userId).exec(),
Product.findById(req.params.productId).exec()
]);
if (!user) {
res.status(404).json({
message: 'User not found'
});
} else if (!product) {
res.status(404).json({
message: 'Product not found'
});
} else {
// *** Both found, do the review
const review = new Review({
_id: mongoose.Types.ObjectId(),
rev: req.body.rev,
productId: req.params.productId,
userId: req.params.userId
});
const response = await review.save();
console.log("responseeeeeeee", response);
res.status(201).json({
response
});
}
} catch (error) {
console.log("error", error);
res.status(500).json({
error
})
}
Note that the entire code is wrapped in a try/catch, so the async function this is in will never reject (unless the console.log or res.send in the catch block raised errors), so it won't result in an unhandled rejection warning if you just make your Express endpoint handler async (whereas normally passing an async function into something that isn't expecting to receive a promise is an anti-pattern). (If you want to be a bit paranoid, wrap the contents of the catch block in another try/catch.)

How can i handle error with redis and bluebird?

I've been facing with problem with redis and async await.
I have old redis.get with callback:
redis.get(token, async (error, result) => {
if (error) {
return res.status(404).json({ msg: 'Confirm token is invalid.' });
}
if (result === null) {
return res.status(400).json({ msg: 'Confirm token is expired.' });
}
})
But i will want to refactor him to async/await
bluebird.promisifyAll(redis.RedisClient.prototype);
bluebird.promisifyAll(redis.Multi.prototype);
const result = async redis.asyncGet(token)
I successfully get the result, BUT how can i get the error ?
Thanks
Having your code, you just need to surround the redis call using async/await syntax in a try/catch statement:
bluebird.promisifyAll(redis.RedisClient.prototype);
bluebird.promisifyAll(redis.Multi.prototype);
try {
const result = redis.get(token);
} catch (e) {
return res.status(400).send({ msg: 'Confirm token is expired.' })
}

Try/Catch catches promise reject... any way around that?

I have to use try/catch because MongoDb's ObjectId() will break/error if arg is to short.
The promise .catch never fires when the try is good...it seems that the outer catch(e) will catch the promise reject(). Is this expected behavior? anyway around that?
function deleteUser(req, res) {
try {
var userId = { '_id': ObjectId(String(req.params.user_id)) };
var petData = _getAllData(userId);
petData
.then(function(doc) {
data.deleteAllData(doc);
result = user.remove(userId);
_returnApi(result, res);
})
.catch(function(err) {
console.log(`error delete user -${err}`);
res.status(500).json({ error: "error deleting user" });
});
}
catch(e) {
res.status(400).json({ error: "user id format incorrect" });
}
}
Per those two issues 1 and 2 are discussed in Mongoose issue site, after mongoose v4.2.5, the Invalid ObjectId could be caught in find method through exec(), here are the sample codes test under mongoose v4.4.2
Foo.find({_id: 'foo'}) // invalid objectId
.exec()
.then(function(doc) {
console.log(doc);
})
.catch(function(err) {
console.log(err);
})
Output:
{ [CastError: Cast to ObjectId failed for value "foo" at path "_id"]
message: 'Cast to ObjectId failed for value "foo" at path "_id"',
name: 'CastError',
kind: 'ObjectId',
value: 'foo',
path: '_id',
reason: undefined }
However, according to this issue, the update method is still failed.
The way I see it, you can reduce it to single catch block, but return different messages based on the error type then:
function deleteUser(req, res) {
let userId;
return Promise.resolve({ '_id': ObjectId(String(req.params.user_id))})
.then(_userId => {
userId = _userId;
return _getAllData(userId);
}).then(doc => {
data.deleteAllData(doc);
result = user.remove(userId);
return _returnApi(result, res);
}).catch(err => {
if(!userId) return res.status(400).json({ error: "user id format incorrect" });
console.log(`error delete user -${err}`);
res.status(500).json({ error: "error deleting user" });
});
}
}

Categories