I have more of a code architecture question about error handling NodeJs Express apps. I am not sure on what is the best pattern for error handling. On that note, what situations should be considered as an error. For instance, is a 401 Unauthorized code considered an error even though this response is expected when sending bad credentials?
When using:
//app.js file
app.use(err, req, res, next){}
I generally tend to only put 5xx errors here which would represent situations in which a database cannot be found or no network connection issue or function failures. As for the rest, I would manually send back a status code, such as a 401, from the controller by explicitly coding res.status(xxx).send(); or something of the sort. But the issue behind what I'm doing is I tend to repeat myself and have to place logging scattered across the app. Is my approach fine? Should I instead be creating multiple error handling middlewares for different ranges of status codes? I need an opnion
I prefer using middleware with your custom error class to deal with this problem.
Let's see a error class, which contains a custom error message, http status code and logLevel if incase you employ logger.
module.exports = class ApiCalError extends Error {
constructor (message, status, logLevel) {
// Calling parent constructor of base Error class.
super(message);
// Capturing stack trace, excluding constructor call from it.
Error.captureStackTrace(this, this.constructor);
// Saving class name in the property of our custom error as a shortcut.
this.name = this.constructor.name;
// You can use any additional properties you want.
// I'm going to use preferred HTTP status for this error types.
// `500` is the default value if not specified.
this.status = status || 400;
this.logLevel = logLevel || 'warn';
}
toResponseJSON () {
return {
success: false,
message: this.message
}
}
};
Now, let's took a look at a controller. We have only sent successful response from this controller, and passed custom errors to middleware.
exports.Login = function(req, res, next) {
const validationResult = validateLoginForm(req.body)
if (!validationResult.success) {
var err = new customError(validationResult.message, 400, 'warn')
return next(err)
} else {
return passport.authenticate('local-login', (err, token, userData) => {
if (err) {
if (err.name == 'IncorrectCredentialsError' || err.name == 'EmailNotVerified') {
var error = new customError(err.message, 400, 'warn')
return next(error)
}
return next(err)
}
return res.json({
success: true,
message: 'You have successfully logged in!',
token,
user: userData
})
})(req, res, next)
}
}
Now, let's take a look at logger and error handlers middlewares. Here, logger will log the errors in api and pass the error to error handlers. These functions would then be used in app.use().
// Import library
var Logger = function(logger) {
return function(err, req, res, next) {
var meta = {
path: req.originalUrl,
method: req.method,
'user-agent': req.headers['user-agent'],
origin: req.headers.origin
}
if (err instanceof customError) {
logger.log(err.logLevel, err.message, meta)
return next(err)
} else {
logger.log('error', err.message, meta)
return next(err)
}
}
}
var ErrorHandler = function() {
return function(err, req, res, next) {
if (err instanceof customError) {
return res.status(err.status).json(err.toResponseJSON())
}else{
return res.status(500).json({
success: false,
message: err.message
})
}
}
}
module.exports = {
Logger,
ErrorHandler
}
Related
I a trying to implement a rest API for our project then I go for node js and express. I have built all the models and controllers. I faced an issue while trying to handle an error. Errorhandler function doesn't receive all the properties of error that caught in try/catch block. I can not read its name in a handler but I can use its name in the controller. Could you please help me?
const errorHandler = (err, req, res, next) => {
console.log(`Error in method:${req.method}: ${err.stack}`.bgRed);
let error = { ...err };
console.log(`Error handler: ${err.name}`);
res.status(error.statusCode || 500).json({
success: false,
data: error.message || 'Server Error',
});
};
module.exports = errorHandler;
controller
const mongoose = require('mongoose');
const Product = require('../models/Product');
const ErrorResponse = require('../utils/error');
const routeName = 'PRODUCT';
// #desc getting single product via id
// #route GET api/v1/products
// #acces public
exports.getProdcut = async (req, res, next) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return next(
new ErrorResponse(`Product not found with id:${req.params.id}`, 404)
);
}
res.status(200).json({
success: true,
data: product,
});
} catch (err) {
console.log(err.name);
console.log('ERRO APPEND');
next(new ErrorResponse(`Product not found with id:${req.params.id}`, 404));
}
};
Assuming that errorHandler is part of your middleware that is somewhere after getProdcut, you can try just throwing the error and Express will automatically detect that for you, because error handling middleware such as yours accepts 4 parameters. So the following would work:
const getProdcut = async (req, res, next) => {
try {
// ...
} catch (err) {
throw err;
}
};
const errorHandler = (err, req, res, next) => {
if (err) {
console.log('hello from the error middleware');
console.log(err.name);
}
else {
// next() or some other logic here
}
}
app.use('/yourRoute', getProdcut, errorHandler);
And inside of your errorHandler you should have access to the error object.
Error-handling middleware always takes four arguments. You must provide four arguments to identify it as an error-handling middleware function. Even if you don’t need to use the next object, you must specify it to maintain the signature. Otherwise, the next object will be interpreted as regular middleware and will fail to handle errors.
https://expressjs.com/en/guide/using-middleware.html#middleware.error-handling
In my mongoose schema, I have a field type that is required. I am using a custom error handler in express defined as
const notFound = (req, res, next) => {
const error = new Error(`Not found-${req.originalUrl}`);
res.status(404);
next(error);
};
const errorHandler = (err, req, res, next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
res.status(statusCode);
res.json({
message: err.message,
stack: process.env.NODE_ENV === 'production' ? null : err.stack,
});
};
and use the error handlers at the bottom of my server.js file as
app.use(notFound);
app.use(errorHandler);
However, when I try to test the route that posts an entry using Postman, the request will be stuck and no response is sent back, and in the terminal there is an error saying that UnhandledPromiseRejectionWarning: ValidationError: ...
My question is: shouldn't my custom error handler catch the error?
Make sure that you're forwarding the error wherever you're performing your mongoose action. There you should be able to forward that to your middleware.
For example you can try something like this:
example.findById(req.id, async function(err, foundRecord){
if(err) {
next(err);
} else {
....
}
});
Let me know if that works.
You can try with this simple HttpError:
Create HttpError.js:
class HttpError extends Error {
constructor(statusCode, message = 'Internal Server Error') {
super(message);
this.statusCode = statusCode;
this.isSuccess = false;
this.isError = true;
this.errorMessage = message;
this.data = null;
}
}
module.exports.HttpError = HttpError;
You can create for an example: NotFoundError extends HttpError :
const { HttpError } = require('../HttpError');
class NotFoundError extends HttpError {
constructor(message = 'Not Found') {
super(404, message);
}
}
module.exports.NotFoundError = NotFoundError;
Now, in your app.js, you can handle error like this code below:
// handle notFoundError
app.use((req, res, next) => { throw new NotFoundError() });
// handle unexpectedly error or another error
app.use((error, req, res, next) => {
res.status(error.statusCode || 500).send(error);
})
If you want to look at the tutorial, for mys, but use the indonesian language ("But you don't have to listen, just look at the concept"), you can check it out here:
Handle Express Error: https://www.youtube.com/watch?v=GwS6KJmO9w8&list=PLREvIK3N7Ga6F669gbDCDMwrn37Uq32-O&index=13
Handle HttpError: https://www.youtube.com/watch?v=VoorNGvDypE&list=PLREvIK3N7Ga6F669gbDCDMwrn37Uq32-O&index=14
Or for example code, you can check it out on this github repo: https://github.com/12bedeveloper/basic-express
Keep learn.
Hey guys I want to achieve central error handling in express, I've done this.
app.use(function(err,req,res,next){
logger.error(err);
utils.jsonOutHandler(err,null,res);
next(err);
});
app.get('/',(req,res)=>{
throw new Error('Testing');
});
Also I made a special jsonOutHandler method which sends proper response to the user.
function jsonOutHandler(err, result, out) {
if (err) {
let status = 500;
const message = 'Something broke';
if (err instanceof DbError) {
status = 500;
}
if (err instanceof ValidationError) {
status = 400;
}
if (err instanceof SystemError) {
status = 500;
}
out.status(status).send({message});
return;
}
out.status(result.status).send({data: result.data});
}
But whenever I throw error on '/' route my error handler is never triggered. Why?
Express is based on middlewares, so, if you wanna catch the errors inside the middleware, you should call the error middleware:
app.get('/',(req,res)=>{
next(new Error('Testing'));
});
/**
* middleware to catch errors happened before this middleware
* express knows this middleware is for error handling because it has
* four parameters (err, req, res, next)
**/
app.use((err, req, res, next) => {
res.status(500).send({
message: err.message,
});
});
I hope you can adapt this example to your requirements. The thing to keep in mind is an error middleware can be used from previous middlewares. In your example you couldn't catch the error because your middleware was defined before your main router app.get('/')
Test
it('should fail trying to GET bookmarks with false user id',async () => {
try {
const response = await request(app)
.get(baseApiUrlUnderTest + 'false_user_id/bookmarks')
.set('Authorization', bearerToken);
} catch (e) {
console.log(e); //it doesn't reach this point
expect(e.httpStatus).to.equal(HttpStatus.UNAUTHORIZED);
}
});
The relevant part of the method under test:
/* GET bookmark of user */
personalBookmarksRouter.get('/', keycloak.protect(), wrapAsync(async (request, response) => {
userIdTokenValidator.validateUserIdInToken(request);
...
}));
where wrapAsync makes sure the error is passed to the custom error handler:
let wrapAsync = function (fn) {
return function(req, res, next) {
// Make sure to `.catch()` any errors and pass them along to the `next()`
// middleware in the chain, in this case the error handler.
fn(req, res, next).catch(next);
};
}
The validateUserIdInToken method which causes the method under test to throw an exception:
const AppError = require('../models/error');
const HttpStatus = require('http-status-codes');
let validateUserIdInToken = function (request) {
const userId = request.kauth.grant.access_token.content.sub;
if ( userId !== request.params.userId ) {
throw new AppError(HttpStatus.UNAUTHORIZED, 'Unauthorized', ['the userId does not match the subject in the access token']);
}
}
module.exports.validateUserIdInToken = validateUserIdInToken;
and the custom error handler in the root middleware:
app.use(function(err, req, res, next) {
if (res.headersSent) {
return next(err)
}
if(err instanceof AppError) { //execution lands here as expected and the test stops...
res.status(err.httpStatus);
return res.send(err);
} else {
res.status(err.status || HttpStatus.INTERNAL_SERVER_ERROR);
res.send({
message: err.message,
error: {}
});
}
});
I think you may be approaching this incorrectly. Invalid auth should not raise errors in the app - it's not an error really, is a validation issue.
If the auth fails, simply send the relevant http error code - 401 back to the client.
res.send(HttpStatus.UNAUTHORIZED, 'a message if you want'); // 401
In your route handler:
personalBookmarksRouter.get('/', keycloak.protect(), wrapAsync(async (request, response) => {
const userId = request.kauth.grant.access_token.content.sub;
if ( userId !== request.params.userId ) {
return response.send(HttpStatus.UNAUTHORIZED);
}
...
}));
In your test, check the for status 401:
chai.request(server)
.get('/false_user_id/bookmarks')
.end((err, result) => {
if (err) {
return callback(err);
}
result.should.have.status(401);
});
Thanks to #laggingreflex's comment I missed debugging that the response actually returned with the expected status and error message
The adjusted test case now looks like this:
it('should fail trying to GET bookmarks with false user id',async () => {
const response = await request(app)
.get(baseApiUrlUnderTest + 'false_user_id/bookmarks')
.set('Authorization', bearerToken);
expect(response.status).to.equal(HttpStatus.UNAUTHORIZED);
});
The http-proxy-middleware Nodejs module provides a way of re-target request using a function in the option.router parameter. As described here:
router: function(req) {
return 'http://localhost:8004';
}
I'll need to implement a process that check some aspect in the request (headers, URLs... all that information is at hand in the req object that function receives) and return a 404 error in some case. Something like this:
router: function(req) {
if (checkRequest(req)) {
return 'http://localhost:8004';
}
else {
// Don't proxy and return a 404 to the client
}
}
However, I don't know how to solve that // Don't proxy and return a 404 to the client. Looking to http-proxy-middleware is not so evident (or at least I haven't found the way...).
Any help/feedback on this is welcomed!
You can do this in onProxyReq instead of throwing and catching an error:
app.use('/proxy/:service/', proxy({
...
onProxyReq: (proxyReq, req, res) => {
if (checkRequest(req)) {
// Happy path
...
return target;
} else {
res.status(404).send();
}
}
}));
At the end I have solved throwing and expection and using the default Express error handler (I didn't mention in the question post, but the proxy lives in a Express-based application).
Something like this:
app.use('/proxy/:service/', proxy({
...
router: function(req) {
if (checkRequest(req)) {
// Happy path
...
return target;
}
else {
throw 'awfull error';
}
}
}));
...
// Handler for global and uncaugth errors
app.use(function (err, req, res, next) {
if (err === 'awful error') {
res.status(404).send();
}
else {
res.status(500).send();
}
next(err);
});