How To Deal With Errors In Express Controller / Service Setup? - javascript

I have a challenge where I can't really make a decision on how to deal with HTTP responses / Errors in my services and controllers in my Express API. My goal is to have the services be responsible for one thing only and not deal with HTTP at all. Atleast that's my thought.
I would love some feedback on my approach...
I have added general error middlewares:
const errorResponder = (error, req, res, next) => {
if (error.statusCode && error.message) {
return res.status(error.statusCode).send(error.message);
}
if (error.statusCode) {
return res.status(error.statusCode).send();
}
if (error.message) {
return res.status(500).send(error.message);
}
return next(error); // Forward if above is't triggered
};
const errorFailSafe = (error, req, res, next) => {
console.log("Fail safe");
res.status(500).send("Something went wrong, we are digging into it!");
};
And then in my controller I unwrap what I need from the req and send to a service. Afterward I send the response back to the client.
findUser: async (req,res,next) => {
const userId = req.params.userId;
try {
// Call service
const user = await UserService.findOne(userId);
// Send user back to client
res.status(200).send(user);
} catch (error) {
return next(error)
}
}
In my service using Sequelize:
findOne: async (userId) => {
try {
let user = await db.users.findByPk(userId);
if (user == null) {
throw new NotFound("User not found");
}
return user;
} catch (error) {
throw error;
}
};
The NotFound error is a custom error class extending Error.
class NotFound extends Error {
constructor(message) {
super(message);
this.statusCode = 404;
}
}
module.exports = NotFound ;
Here I kinda break the seperation by having the Service deal with HTTP by calling NotFound.
I could change this so it's the Controller doing the check. Would that be better?
Any feedback would be appreciated. :)

Related

Catch errors when using async/await

I have a register function inside my Express application to create a new user. Inside this function there are a few tasks: create the user in Auth0, send an email, send a response to the client.
I want to be able to catch the errors coming from Auth0 or Postmark to send back specific errors to the client and log them to the console. I though I could achieve this by adding a catch to an await function (I want to avoid a waterfall of .then() and .catch() blocks). This sends the error to the client but doesn't stop the code from executing. The email part is still trying to execute while the user object is undefined and I'm getting the error Cannot set headers after they are sent to the client.
How can I fix this by keeping the async/await functionality and keep the seperate error handling for each action?
Register function
export const register = asyncHandler(async (req, res, next) => {
// Create user in Auth0
const user = await auth0ManagementClient.createUser({
email: req.body.email,
password: generateToken(12),
verify_email: false,
connection: 'auth0-database-connection'
}).catch((error) => {
const auth0_error = {
title: error.name,
description: error.message,
status_code: error.statusCode
}
console.log(auth0_error);
if(error.statusCode >= 400 && error.statusCode < 500) {
return next(new ErrorResponse('Unable to create user', `We were unable to complete your registration. ${error.message}`, error.statusCode, 'user_creation_failed'));
} else {
return next(new ErrorResponse('Internal server error', `We have issues on our side. Please try again`, 500, 'internal_server_error'));
}
});
// Send welcome mail
await sendWelcomeEmail(user.email)
.catch((error) => {
const postmark_error = {
description: error.Message,
status_code: error.ErrorCode
}
console.log(postmark_error);
if(error.statusCode >= 400 && error.statusCode < 500) {
return next(new ErrorResponse('Unable to send welcome email', `We were unable to send a welcome email to you`, error.statusCode, 'welcome_email_failed'));
} else {
return next(new ErrorResponse('Internal server error', `We have issues on our side. Please try again`, 500, 'internal_server_error'));
}
});
res.status(201).json({
message: 'User succesfully registered. Check your mailbox to verify your account and continue the onboarding.',
data: {
user
}
});
});
asyncHandler.js
const asyncHandler = fn => ( req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
export default asyncHandler;
I'd use try/catch blocks, but declare the user variable outside the try scope.
async function handler(req, res, next) {
let user;
try {
user = await auth0ManagementClient.createUser(...);
} catch (error) {
return next(new ErrorResponse(...));
}
try {
await sendWelcomeEmail(user.email);
} catch (error) {
return next(new ErrorResponse(...));
}
res.status(201).json(...);
}
return will only terminate the current function. Here, the function that gets terminated by the return is the .catch() callback.
In your example and if you want to stick to using Promise.then().catch() you can check for the user value as the catch() callback will return its value in it.
The easier way would be to use try/catch blocks to interrupt the whole controller with the return statement.

How do I send more than one error at a time when validating?

When I create a POST request for I need to validate the following fields: first_name, last_name, mobile_number, reservation_date, reservation_time and people(party size).
Right now I have a middleware function that checks if any of the fields are missing:
function hasProperties(...properties) {
return function (res, req, next) {
const { data = {} } = res.body;
try {
properties.forEach((property) => {
if (!data[property]) {
const error = new Error(`${property}`);
error.status = 400;
throw error;
}
});
next();
} catch (error) {
next(error);
}
};
}
Then in my controller:
const hasAllProps = hasProperties(
'first_name',
'last_name',
'mobile_number',
'reservation_date',
'reservation_time',
'people'
);
This is working great however I have to add additional validation to several of the fields. I have 2 additional functions: one is making sure the people field is a number, and the other is making sure the reservation_date is a date:
const validPeople = (req, res, next) => {
const { people } = req.body;
if (Number.isInteger(people)) {
return next();
}
next({ status: 400, message: 'people' });
};
const validDate = (req, res, next) => {
const { reservation_date } = req.body;
if (reservation_date instanceof Date) {
return next();
}
next({ status: 400, message: 'reservation_date' });
};
Then I pass them all in to my exports:
create: [hasAllProps, validDate, validPeople]
I am only ever able to send one error at a time, in this case its validDate because it comes before validPeople in the exports array. I am unable to throw all of my errors into an array because I need to response with:
status: 400, message: '<the-specific-field>'
Is there a way to individually send all these error messages?
As the other response has stated, if you're trying to send multiple responses, that's not possible. You can, however, construct an array of the errors.
You could technically pass data between middleware... (Can I send data via express next() function?)
... but my recommendation would be to be to try to merge them into a single middleware. For example, hasAllProps, validPeople, and validDate should ideally all take in a req and return null or an error. Then you could do:
function validDate(req) {
return null;
}
function validOtherProp(req) {
return 'error_here';
}
function anotherValidation(req) {
return 'second_error';
}
const errorCollectorMiddleware = (...validators) =>
(req, res, next) => {
const errors = validators.map(v => v(req)).filter(error => error !== null);
if (errors.length > 0) {
next({
status: 400,
errors
})
} else {
next();
}
}
// This is how you construct a middleware
const middleware = errorCollectorMiddleware(validDate, validOtherProp, anotherValidation);
// And here's a test. You wouldn't do this in your actual code.
console.log(middleware(null, null, console.log))
/*
{
"status": 400,
"errors": [
"error_here",
"second_error"
]
}
*/
With HTTP/S you cannot have one request two responses. The client system sends the request, receives the response and does not expect a second response.

Express/Postgres User registration controller

To simplify the situation I'll just post the following controller for an express route which interactions with a Postgres Database. My question is about error handling. If an error occurs it will be caught within the catch clause. But how can I access the errors thrown by the database queries itself. If I make several await several queries and one of them fails I need probably to restore stuff in the database? For example if the insertion of the user in the user table is a success, but the following query of inserting the user in another table fails, I need to delete the user from the user table again. How does one model such flows?
//
// Register User
//
export const registerUser = async (request, response, next) => {
try {
const usersWithSameMail = await client.query(`SELECT * FROM public.users WHERE email = '${user.email}'`);
if(usersWithSameMail.rows.length > 0){
return response.status(403).json({"code": "ERROR", "message": "Email is already registered"})
} else {
await client.query(`
INSERT INTO public.users(first_name, last_name, email, password)
VALUES ('${user.first_name}', '${user.last_name}', '${user.email}', crypt('${user.password}', gen_salt('bf', 8)));
`);
// more await statements...
return response.status(200).json({"code": "INFO", "message": "Verification mail sent to user"});
}
} catch (error) {
return response.status(500).json({"code": "ERROR", "message": "Error occured while registering the user. Please try again."});
}
}```
You can use middlewares chaining your routes handler. In order to it work, you will have to change your current working code to use Single-responsibility principle. Do only one responsability per middleware and chain all handlers to work as one.
Lets say you want to insert new user, to perform this operation we should:
lookup if email is unique
hash password
Insert new user
return inserted data in postgres back as a response
Following the middleware chaining we should implement a function for each action and chain each action in route definition:
const postgres = require('../../lib/postgres');
const crypto = require('crypto');
exports.insertedData = (req, res) => {
res.status(200).json(req.employee);
};
exports.hashPassword = (req, res, next) => {
crypto.scrypt(req.body.password.toString(), 'salt', 256, (err, derivedKey) => {
if (err) {
return res.status(500).json({ errors: [{ location: req.path, msg: 'Could not hash password'}] });
}
req.body.kdfResult = derivedKey.toString('hex');
next();
});
};
exports.lookupEmailUnique = (req, res, next) => {
const sql = 'SELECT e.email FROM public.users e WHERE e.email=$1';
postgres.query(sql, [req.body.email], (err, result) => {
if (err) {
return res.status(500).json({ errors: [{ location: req.path, msg: 'Could not query database' }] });
}
if (result.rows.length > 0) {
return response.status(403).json({"code": "ERROR", "message": "Email is already registered"})
}
next()
});
}
exports.insertNewUser = (req, res, next) => {
const sql = 'INSERT INTO public.users(first_name, last_name, email, password) VALUES ($1,$2,$3,$4} RETURNING *';
postgres.query(sql, [req.body.first_name, req.body.last_name, req.body.email, req.body.kdfResult], (err, result) => {
if (err) {
return res.status(500).json({ errors: [{ location: req.path, msg: 'Could not query database'}] });
}
req.employee = result.rows[0];
next();
});
};
here is your route declaration:
const router = require('express').Router();
const userService = require('../controllers/user.controller');
router.post('/register', userService.lookupEmailUnique, userService.hashPassword, userService.insertNewUser, userService.insertedData);
module.exports = router;
Here in routes you are using the middeware to do the chaning, you only pass the control to next middleware if all conditions are met and has full control from database erros.
In my example I do not used the async/await but I can change my example to have a version using async/await.
example middleware with transaction
exports.deletePostagem = async (req, res, next) => {
try {
await postgres.query('BEGIN');
const sql2 = 'UPDATE comentario SET postagem = null WHERE postagem = $1';
await postgres.query(sql2, [req.params.id]);
const sql3 = 'DELETE FROM postagem WHERE id = $1';
await postgres.query(sql3, [req.params.id]);
await postgres.query('COMMIT');
res.status(204).json();
res.end();
} catch (err) {
await postgres.query('ROLLBACK');
return res.status(500).json({ errors: [{msg: 'Could not perform operation' }]})
}
}
I used this only as an example, but in my projects I always have a middeware for validate/sanitize the data that comes in request before using in database query prepared statements.
based on transaction documentation in node.js you can use rollback
export const registerUser = async (request, response, next) => {
try {
let error = null;
const client; // create a client, connect to the db
try {
await client.query("begin");
await client.query("first query");
await client.query("second query");
await client.query("third query");
await client.query("commit"); //do commit when is finished all queries
} catch (error) {
error = error;
await client.query("rollback");
} finally {
client.release(); // close the connection
}
if (error) {
return response.status(500).json({ message: error }); // error message
}
return response.status(200).json({ message: "My message" }); // success message
} catch (err) {
return response.status(500).json({ message: err });
}
}

Unable to modified/appened REQUEST in middleware

I am new to nodejs and typescript and I want to add a new parameter in req.body say req.body.jwt_token.
I am using a middleware to update the request data model. The issue is that i am able to access (console.log works) the new key req.body.jwt_token just work in that function and is not accessible(don't even exist) apart from that.
I want to use req.body.jwt_token in some controller.
export function httpsProtocol(req: Request, res: Response, next: NextFunction) {
try {
if (req.headers.authorization != undefined) {
let authorization = req.headers.authorization;
let authorizationArr: string[] = authorization.split('Bearer')
if (authorizationArr[1] != undefined) {
let jwtToken = "Bearer " + authorizationArr[1].trim();
req.headers.Authorization = jwtToken;
req.body.jwt_token = authorizationArr[1].trim();
console.log(req.body.jwt_token); //able to console this
}
}
} catch (error) {
return res.status(422).json({
message: "something goes wrong",
error: error
});
}
next();
};
Please suggest the solution for this problem. how can i achieve this in nodejs and typescript. I am using express as framework
Thank You
💡 The only reason why you can not access req.body.jwt_token in your controller is that before you set the value, you next().
👨🏽‍🏫 Make sure to add your next() inside if/else condition. So, you can copy this code below 👇 and use it:
export function httpsProtocol(req: Request, res: Response, next: NextFunction) {
try {
if (req.headers.authorization != undefined) {
let authorization = req.headers.authorization;
let authorizationArr: string[] = authorization.split('Bearer')
if (authorizationArr[1] != undefined) {
let jwtToken = "Bearer " + authorizationArr[1].trim();
req.headers.Authorization = jwtToken;
req.body.jwt_token = authorizationArr[1].trim();
console.log(req.body.jwt_token); //able to console this
// your next here
next();
} else {
next(); // next or do some stuff
}
} else {
next(); // next or do some stuff
}
} catch (error) {
return res.status(422).json({
message: "something goes wrong",
error: error
});
}
// next(); your next here only make your req.body.jwt_token is undefined
};
Maybe this answer will help you to know the reason: passing value from middleware to controller in restify using req.data is not working?
I hope it can help you 🙏.

Intermittent authentication errors in Cloud Functions

I have a simple Cloud Function that receives a webhook and then does a streaming insert into BigQuery. The code is based on this sample (except that I am using streaming inserts)
exports.webHook = function webHook (req, res) {
return Promise.resolve()
.then(() => {
if (req.method !== 'POST') {
const error = new Error('Only POST requests are accepted');
error.code = 405;
throw error;
}
const events = req.body || {};
if (events) {
const opts = { ignoreUnknownValues: true };
bigquery
.dataset('config.DATASET')
.table('config.TABLE')
.insert(events, opts)
.then((data) => {
console.log(`Success: ${JSON.stringify(data[0])}`);
})
.catch((error) => {
if (error.name === 'PartialFailureError') {
console.error(`PARTIAL ERROR: ${JSON.stringify(error)}`);
} else {
console.error(`OTHER ERROR: ${JSON.stringify(error)}`);
}
});
};
})
.then(() => res.status(200).end())
.catch((err) => {
console.error(err);
res.status(err.code || 500).send(err);
return Promise.reject(err);
});
};
This function works well most of the time, but I do get the occasional authentication error, which then goes away.
textPayload: "OTHER ERROR: {"code":401,"errors":[{"message":"Request
had invalid authentication credentials. Expected OAuth 2 access token,
login cookie or other valid authentication credential. See
https://developers.google.com/identity/sign-in/web/devconsole-project.","domain":"global","reason":"unauthorized"}],"message":"Request
had invalid authentication credentials. Expected OAuth 2 access token,
login cookie or other valid authentication credential. See
https://developers.google.com/identity/sign-in/web/devconsole-project."}"
I am not sure how auth could be an issue since the Cloud Function and BigQuery are all in the same project.
The folks on the Cloud Functions team think this may be due to an issue with the access token time-to-live (TTL) and suggested a workaround that has worked for me. Instead of initializing BigQuery at the top of your code (as all their examples have it), put the initializing code right inside the function that makes the call.
Do this:
exports.webHook = function webHook (req, res) {
const bigquery = require('#google-cloud/bigquery')();
return Promise.resolve()
.then(() => {
if (req.method !== 'POST') {
const error = new Error('Only POST requests are accepted');
error.code = 405;
throw error;
}
.
.
instead of:
const bigquery = require('#google-cloud/bigquery')();
.
.
exports.webHook = function webHook (req, res) {
return Promise.resolve()
.then(() => {
if (req.method !== 'POST') {
const error = new Error('Only POST requests are accepted');
error.code = 405;
throw error;
}
.
.

Categories