Firebase Function Promise Chain not catching errors - javascript

I'm using a Firebase function that detects when a Stripe Token is added to a user collection, which then creates a Stripe User and creates a Subscription for that user.
The problem I'm having is that the below code has an error in it, and whilst I need to figure out what that error is, the Promise Chain doesn't seem to catch the actual error.
All that logs out is Function execution took 4375 ms, finished with status: 'connection error'
Does anyone know why this is happening. The only way I can get it to log catch the error is to nest the Promises.
exports.createStripeUser = functions.firestore
// A Stripe Token is created in a user, therefore they have agreed to pay
// Create a User in Stripe, and then attach the subscription to the Stripe customer
.document('users/{userId}/stripe/{stripeCollectionId}')
.onCreate((snap, context) => {
const stripeToken = snap.data().stripeToken;
let customer;
return admin.auth().getUser(`${context.params.userId}`)
.then(userObj => {
const user = userObj.toJSON();
return stripe.customers.create({
email: user.email,
source: stripeToken
})
})
.then(cust => {
customer = cust;
return db.collection('users').doc(context.params.userId).collection('plan').doc(context.params.userId).get()
})
.then(plan => {
return stripe.subscriptions.create({
customer: customer.id,
items: [{plan: plan.data().plan}]
})
})
.catch(e => {
console.log("ERRR", e)
throw new functions.https.HttpsError('unknown', e.message, e);
})
})

Wrap your block in a
try {
}
catch (e) {
// e holds the nested error message
}
block and pick up the error there.

Related

Is it safe to call a throwing function inside a catch block?

exports.createUser = functions.https.onCall(async (data, _context) => {
const email = data.email;
const password = data.password;
try {
// First, create the user account.
const userRecord = await admin.auth().createUser({
email: email,
password: password,
});
// User successfully created, now update the database.
const userId = userRecord.uid;
const db = admin.firestore();
const batch = db.batch();
batch.create(
db.collection("userAccounts").doc(userId), {created: admin.firestore.FieldValue.serverTimestamp()},
);
await batch.commit();
// Database successfully updated, now return the newly-created userId.
return Promise.resolve(userId);
} catch (error) {
if (userId) {
// There was an error updating the database. However, a user was created
// beforehand. Therefore, delete the user before terminating the
// function.
admin.auth().deleteUser(userId); // <-- this throws
throw new functions.https.HttpsError("unknown", "Database error", error);
}
throw new functions.https.HttpsError("unknown", "Authentication error", error);
}
});
This function performs two async tasks: creates a user and updates a database. If the database update fails, I need to undelete the user. This function deletes the user in the catch block when it catches a database error. However, deleting the user itself throws. If the delete-user function fails, would that error be caught in this catch block and possibly create an infinite loop? How best to handle this scenario?
It will not create an infinite loop, but the error won't be caught either - which should be avoided. You'll need to tack on another .catch, because you also want to throw a different error afterwards.
} catch (error) {
if (userId) {
// There was an error updating the database. However, a user was created
// beforehand. Therefore, delete the user before terminating the
// function.
admin.auth().deleteUser(userId)
.catch(() => {}); // Ignore errors thrown by deleteUser
throw new functions.https.HttpsError("unknown", "Database error", error);
}
throw new functions.https.HttpsError("unknown", "Authentication error", error);
}
You should also be sure that the caller of this callback passed to onCall catches its asynchronous errors to avoid unhandled rejections.
Still, this process is pretty suspect - it seems odd to be deliberately calling a function you think will probably throw. If you need to call deleteUser only in the case that createUser succeeds, checking for the userId looks like the right approach, but that'll only work if the variable is in scope at that point, which it isn't. You can change your code to:
exports.createUser = functions.https.onCall(async (data, _context) => {
const { email, password } = data;
let userId; // <---------------------------
try {
// First, create the user account.
const userRecord = await admin.auth().createUser({ email, password });
// User successfully created, now update the database.
userId = userRecord.uid; // <---------------------------
const db = admin.firestore();
const batch = db.batch();
batch.create(
db.collection("userAccounts").doc(userId), {created: admin.firestore.FieldValue.serverTimestamp()},
);
await batch.commit();
// Database successfully updated, now return the newly-created userId.
return userId; // don't need to wrap this in a Promise
} catch (error) {
if (userId) {
// There was an error updating the database. However, a user was created
// beforehand. Therefore, delete the user before terminating the
// function.
admin.auth().deleteUser(userId)
.catch(() => {}); // This SHOULDN'T throw, but just in case
throw new functions.https.HttpsError("unknown", "Database error", error);
}
throw new functions.https.HttpsError("unknown", "Authentication error", error);
}
});
Or to
exports.createUser = functions.https.onCall(async (data, _context) => {
const { email, password } = data;
let userId;
try {
// First, create the user account.
const userRecord = await admin.auth().createUser({ email, password });
userId = userRecord.uid;
} catch (error) {
throw new functions.https.HttpsError("unknown", "Authentication error", error);
}
try {
// User successfully created, now update the database.
const db = admin.firestore();
const batch = db.batch();
batch.create(
db.collection("userAccounts").doc(userId), { created: admin.firestore.FieldValue.serverTimestamp() },
);
await batch.commit();
// Database successfully updated, now return the newly-created userId.
return userId;
} catch (error) {
// There was an error updating the database. However, a user was created
// beforehand. Therefore, delete the user before terminating the
// function.
admin.auth().deleteUser(userId);
throw new functions.https.HttpsError("unknown", "Database error", error);
}
});

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' });
});
};

How to catch a Typeorm transaction error in NestJs

I have a server side written in node.js and nestJs, querying with typeorm.
I'm trying to wrap some query's to the database with transaction as suggested here with some changes inspired by typeorm's docs, like this:
getManager().transaction(async transactionalEntityManager => {
transactionalEntityManager.save<Entity>(newEntity)
transactionalEntityManager.save<Entity1>(newEntity1)
});
The transaction works well and rollback the database if there was an error.
tested this way:
getManager().transaction(async transactionalEntityManager => {
transactionalEntityManager.save<Entity>(newEntity)
throw 'There is an error'
transactionalEntityManager.save<Entity1>(newEntity1)
});
The execution of the transaction is inside a graphQL Mutation, so I should return an error to the client if something went wrong, the problem is that I can't catch the errors from the transaction.
Tried doing this:
#Mutation(returns => Entity)
async create(): Promise<Entity> {
let entity = null;
let error = null;
getManager().transaction(async transactionalEntityManager => {
try {
entity = await transactionalEntityManager.save<Entity>(newEntity)
await transactionalEntityManager.save<Entity1>(newEntity1);
} catch (err) {
error = err
}
})
if (error) {
return error
}
return entity
}
when I throw error I catch it successfully, but when a real error occurs I can console.log() it in the server but it never reaches to return to the client.
You are not awaiting your transaction to finish thus, the exception can be thrown after your function call end and you don't get the exception.
Just await your transaction and it should be fine.
await getManager().transaction(async transactionalEntityManager => {
...
throw 'ERROR THAT SHOULD BE CATCHED'
}
Also it returns the result of the inner function so it can be useful.
const ok = await getManager().transaction(async transactionalEntityManager => {
await transactionalEntityManager.save<Entity>(newEntity)
await transactionalEntityManager.save<Entity>(newEntity2)
return 'OK'
}

Express.js and Bluebird - Handling the promise chain

In a backend API I have a login route which should perform the following sequence of actions:
Given an username and password, try to authenticate the user against an Active Directory. If authentication has failed reply with status 401. If success, continue.
Look for an user with the given username in the database. If not found reply with status 403, otherwise continue.
Find if the user document has some details like email, display name, etc (in case this is not the first time logging in). If yes reply with the user object, otherwise continue.
Get user details from the Active Directory and update the user object in the database. Reply with the updated object.
Code:
router.post('/login', (req, res, next) => {
// capture credentials
const username = req.body.username;
const password = req.body.password;
let user = null;
// authenticate
ad.authenticate(username, password)
.then((success) => {
if (!success) {
res.status(401).send(); // authentication failed
next();
}
return User.findOne({ username }).exec();
})
.then((found) => {
if (!found) {
res.status(403).send(); // unauthorized, no account in DB
next();
}
user = found;
if (user.displayName) {
res.status(201).json(user); // all good, return user details
next();
}
// fetch user details from the AD
return ad.getUserDetails(username, password);
})
.then((details) => {
// update user object with the response details and save
// ...
return user.save();
})
.then((update) => {
res.status(201).json(update); // all good, return user object
next();
})
.catch(err => next(err));
});
Now I had this running with callbacks but it was really nested. So I wanted to give Bluebird promises a try, but I have two problems:
Looks chaotic, any better way to chain the calls and handle responses?
Whenever I call next() to stop the request after replying, the execution continues to the other .then(). Although the client receives the correct response, in the server log I find that the execution have continued. For example, if there is no account in DB for a given user, the client receives the 403 response but in the server log I see an exception failed to read property displayName of null, because there was no user and it should have stopped in the next() after res.status(403).send();.
Best use if/else to make clear what branches will execute and which won't:
ad.authenticate(username, password).then((success) => {
if (!success) {
res.status(401).send(); // authentication failed
} else {
return User.findOne({ username }).exec().then(user => {
if (!user) {
res.status(403).send(); // unauthorized, no account in DB
} else if (user.displayName) {
res.status(201).json(user); // all good, return user details
} else {
// fetch user details from the AD
return ad.getUserDetails(username, password).then(details => {
// update user object with the response details and save
// ...
return user.save();
}).then(update => {
res.status(201).json(update); // all good, return user object
});
}
});
}
}).then(() => next(), err => next(err));
The nesting of then calls is quite necessary for conditional evaluation, you cannot chain them linearly and "break out" in the middle (other than by throwing exceptions, which is really ugly).
If you don't like all those then callbacks, you can use async/await syntax (possibly with a transpiler - or use Bluebird's Promise.coroutine to emulate it with generator syntax). Your whole code then becomes
router.post('/login', async (req, res, next) => {
try {
// authenticate
const success = await ad.authenticate(req.body.username, req.body.password);
if (!success) {
res.status(401).send(); // authentication failed
} else {
const user = await User.findOne({ username }).exec();
if (!user) {
res.status(403).send(); // unauthorized, no account in DB
} else if (user.displayName) {
res.status(201).json(user); // all good, return user details
} else {
// fetch user details from the AD
const details = await ad.getUserDetails(username, password);
// update user object with the response details and save
// ...
const update = await user.save();
res.status(201).json(update); // all good, return user object
}
}
next(); // let's hope this doesn't throw
} catch(err) {
next(err);
}
});
To answer your second point, you have to reject your promise after calling next() (or at least return something, otherwise the line after will be executed). Something like
next();
return Promise.reject()
and change your catch so it works if you do not have an error
.catch(err => {
if (err)
next(err)
});
To your second question first: there is no way to break/stop a promise chain, unless your callback throw err like
doAsync()
.then(()=>{
throw 'sth wrong'
})
.then(()=>{
// code here never runs
})
You can simply try below demos to verify the second callback still runs.
doAsync()
.then(()=>{
res.end('end')
})
.then(()=>{
// code here always runs
})
doAsync()
.then(()=>{
return;
})
.then(()=>{
// code here always runs
})
To your first question: to use the second parameter in then(), which means reject. And each time split the logic to two parts.
var p = new Promise(function(resolve, reject) {
return
ad.auth(username, password).then(()={
// check if 401 needed. If needed, return reject
if (dont needed 401 in your logic)
resolve(username)
else
reject({ msg: 'authentication has failed', status: 401 })
})
});
p
.then( (username)=>{
// this only runs when the previous resolves
return User.findOne({ username }).exec()
}, (data)=>{
// in fact in your case you dont even have to have the reject callback
return data
} )
.then( (found)=>{
return
new Promise(function(resolve, reject) {
if (found && /*your logic to determine it's not 403*/)
resolve(user)
else
reject({ msg: 'unauthorized, no account in DB', status: 403 })
})
} )
.then( (found)=>{
return
new Promise(function(resolve, reject) {
if (found && /*your logic to determine it's not 403*/)
resolve(user)
else
reject({ msg: 'unauthorized, no account in DB', status: 403 })
})
} )
.then( (user)=>{
return
new Promise(function(resolve, reject) {
if (/*your logic to determine it has the full info*/)
resolve(user)
else
return ad.getUserDetails(username, password)
})
} )
.then( (user)=>{
// all is good, do the good logic
}, (data)=>{
// something wrong, so here you can handle all the reject in one place
res.send(data)
} )

Categories