Reputation: 41
I have a working apollo graphql express server. The only problem is express is complaining that I don't have a catch block on a promise I'm using to verify a jwt token:
(node:96074) 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()
I can add a catch block to the promise but it then returns pending
instead of rejected
when the token is invalidated. Which causes the authentication flow to break as my graphql resolvers rely on that rejection to block access to the db.
Fwiw this is how auth0, who I'm using for auth, recommends setting it up. They just don't mention the UnhandledPromiseRejectionWarning.
The code looks like this:
//server def
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
if (req.headers.authorization) {
const token = req.headers.authorization.split(' ')[1];
//THE PROMISE IN QUESTION
const authUserObj = new Promise((resolve, reject) => {
jwt.verify(token, getKey, options, (err, decoded) => {
if (err) {
reject(err);
}
if (decoded) {
resolve(decoded);
}
});
});
return {
authUserObj
};
}
},
introspection: true,
playground: true
});
//a graphql resolver that gets the rejection via authUserObj and catches the error
addUser: async (parent, args, {authUserObj}) => {
try {
const AuthUser = await authUserObj;
const response = await User.create(args);
return response;
} catch(err) {
throw new AuthenticationError('You must be logged in to do this');
}
}
That all works... except for that nagging node error I wish to vanquish! So I add a catch block to the promise:
const authUserObj = new Promise((resolve, reject) => {
jwt.verify(token, getKey, options, (err, decoded) => {
if (err) {
console.log("-------rejected-------", err.message)
reject(err);
}
if (decoded) {
console.log("-------decoded-------")
resolve(decoded);
}
});
}).catch( err => { return err.message});
And now instead of authUserObj returning rejected it's pending and anyone will be able to add a user, which kind of defeats the purpose of auth.
If anyone knows how to catch that error while still rejecting it I'm all ears. Thanks.
Upvotes: 0
Views: 397
Reputation: 664920
The problem is less about the unhandled promise rejection and more about the unhandled promise in general. You try to put a promise inside the context
object and then await
the promise in the addUser
resolver only. In other resolvers, the promise might not be used at all, and when the jwt verification fails for those the rejection will go unhandled. (Also if the resolvers are executed asynchronously, the promise might be rejected before they can handle it).
Instead, the whole context
initialisation should be done asynchronously, returning a promise for the context object with the user details. This means that the request will fail before even starting to execute a query:
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
if (req.headers.authorization) {
const token = req.headers.authorization.split(' ')[1];
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, options, (err, decoded) => {
if (err) reject(err);
else resolve(decoded);
});
}).then(authUser => {
if (authUser) return { authUser };
// else return {};
});
// .catch(err => { … }) - you may chose to ignore verification failure,
// and still return a context object (without an `authUser`)
}
// else return {}; - when not sending the header, no token will be checked at all
},
introspection: true,
playground: true
});
// a graphql resolver that checks for the authUserObj
addUser: async (parent, args, {authUserObj}) => {
if (!authUserObj) { // you might also want to check specific claims of the jwt
throw new AuthenticationError('You must be logged in to do this');
}
const response = await User.create(args);
return response;
}
Upvotes: 1
Reputation: 707686
Just like with try/catch
, a .catch()
will change the promise chain from rejected to resolved if you just return a normal value from the .catch()
handler (or return nothing). When you return a "normal" value, the rejection is considered "handled" and the promise chain becomes resolved with that new value. That's how you handle errors and continue normal processing.
To keep the promise chain rejected, you have to either throw
or return a rejected promise. That will keep the promise chain as rejected.
So, if you want authUserObj
to keep the promise rejected, then change this:
}).catch( err => { return err.message});
to this:
}).catch( err => { return Promise.reject(err.message)});
or something similar that either throws an error or returns a rejected promise.
Upvotes: 0