nodejs middleware execution continues after return - node.js

I have the following middleware:
const mongoose = require('mongoose');
module.exports = function(req, res, next) {
const keys = Object.keys(req.params);
keys.forEach(elem => {
if (
(elem.includes('id') || elem.includes('Id')) &&
!mongoose.Types.ObjectId.isValid(req.params[elem])
)
return res
.status(400)
.json({ msg: `id: ${req.params[elem]} is invalid` });
});
next();
};
it is called in a get request:
// #route GET api/movies/:id
// #desc Get a movie with specified id from db
// #access Public
router.get('/:id', checkId, async (req, res) => {
const movie = await Movie.findById(req.params.id);
res.json(movie);
});
When I make a request in postman with an invalid id (ex: 1234) I recieve the proper response being 400 with msg: 'id 1234 is invalid' yet the execution still passes to the reqest callback code and an error is thrown as I try to access db with invalid id.
so the question is why does the middleware still allow the execution of next() even if it already returned with the 400?

You need to tell the router that there was a problem by calling next("some-error"). For example, you could do this:
module.exports = function(req, res, next) {
const keys = Object.keys(req.params);
keys.forEach(elem => {
if (
(elem.includes('id') || elem.includes('Id')) &&
!mongoose.Types.ObjectId.isValid(req.params[elem])
) {
res
.status(400)
.json({ msg: `id: ${req.params[elem]} is invalid` });
return next("invalidinput");
}
});
next();
};
Or you could be more generic by setting the result outside in the router if you like, like this:
In your middleware:
module.exports = function(req, res, next) {
const keys = Object.keys(req.params);
keys.forEach(elem => {
if (
(elem.includes('id') || elem.includes('Id')) &&
!mongoose.Types.ObjectId.isValid(req.params[elem])
) {
// === Report the error and let the router handle it
return next({
type: "invalidinput",
msg: `id: ${req.params[elem]} is invalid`
);
}
});
next();
};
Then at the bottom in your router:
// handle any errors
router.use(err, req, res, next) => {
if (err) {
if (err.type === "invalidinput") {
return req.status(400).json({msg: err.msg});
}
else {
return res.status(500).json({msg: "Internal error."});
}
}
return next();
}

another possible solution here is to convert the forEach to a classical for loop, thu making this middleware run synschronously
module.exports = function(req, res, next) {
const keys = Object.keys(req.params);
for (let i = 0; i < keys.length; i++) {
if (
(keys[i].includes('id') || keys[i].includes('Id')) &&
!mongoose.Types.ObjectId.isValid(req.params[keys[i]])
)
return res
.status(400)
.json({ msg: `id: ${req.params[keys[i]]} is invalid` });
}
next();
};

Related

'req.params.id' is only accessed by first two middleware functions, but not the third. Why?

Here is the code snippet in index.js:
const { loginRequired, ensureCorrectUser } = require("./middlewares/auth");
const tasks = require("./routes/task");
app.use("/api/tasks/:id", loginRequired, ensureCorrectUser, tasks);
Here, only loginRequired and ensureCorrectUser can access the :id, but tasks can't...
Here is the code snippet from the router:
const { getTasks, addTask } = require("../handlers/task");
const router = express.Router();
router.route("/").get(getTasks).post(addTask);
module.exports = router;
auth.js:
exports.loginRequired = function (req, res, next) {
try {
let token = req.headers.authorization.split(" ")[1];
jwt.verify(token, process.env.SECRET_KEY, function (err, decoded) {
if (decoded) {
return next();
} else {
return next({
status: 401,
message: "Please login first",
});
}
});
} catch (err) {
return next({
status: 401,
message: "Please login first",
});
}
};
exports.ensureCorrectUser = function (req, res, next) {
try {
let token = req.headers.authorization.split(" ")[1];
jwt.verify(token, process.env.SECRET_KEY, function (err, decoded) {
if (decoded && decoded.id === req.params.id) {
console.log(req.params.id); //prints correct id
return next();
} else {
return next({
status: 401,
message: "Unauthorized!",
});
}
});
} catch (err) {
return next({
status: 401,
message: "Unauthorized!",
});
}
};
handler snippet:
exports.getTasks = async function (req, res, next) {
await db.User.findById(req.params.id)
.then((data) => {
console.log(req.params); //prints empty object
res.status(200).json([...data.tasks]);
})
.catch((err) => next(err));
};
exports.addTask = async function (req, res, next) {
try {
let user = await db.User.findById(req.params.id);
console.log(req.params); //prints empty object
user.tasks.push(req.body);
await user.save();
return res.status(200).json({ message: "Task Added!" });
} catch (err) {
next(err);
}
};
Why is it so..? Please help..
This thing worked finally. But it's quite messy.
router
.route("/:id")
.get(loginRequired, ensureCorrectUser, getTasks)
.post(loginRequired, ensureCorrectUser, addTask);
router.delete("/id/:id2", loginRequired, ensureCorrectUser, removeTask);
router.post("/id/complete/:id2", loginRequired, ensureCorrectUser, setComplete);

express-validator isDate and isISO8601 are always false

I'm trying to validate a date. I have tried everything I can but I have not found a solution. Input {"dob": "2002-10-02"}
'use strict'
var validator = require('validator');
var controller = {
create: (req,res) =>{
//pick parameters
var parameters = req.body;
//validator
try {
//not working (always returns false)
//var validate_dob = validator.isDate(parameters.dob + '');
//also not working (always returns false)
//var validate_dob = validator.isISO8601(parameters.dob + '');
} catch (error) {
return res.status(400).send({
message: error
});
}
}
}
In your question you mention tag express-validator, but in your middleware you use pure validator.
Here I am putting an example using the express-validator lib (version 6.6.0). To use validate body parameters (login and password). But you can get the idea and pick the validation for your date from the validators list. Reference.
server/validators/login.validator.js
const { body, validationResult } = require('express-validator');
exports.validationBodyRules = [
body('login', 'login is required').exists(),
body('password', 'password is required').exists(),
body('login', 'login is required').notEmpty(),
body('password', 'password is required').notEmpty()
];
exports.checkRules = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
Here is the routes file
server/routes/login.route.js
const router = require('express').Router();
const loginService = require('../controllers/login.controller');
const loginValidator = require('../validators/login.validator');
router.post('/login', loginValidator.validationBodyRules, loginValidator.checkRules, loginService.hashPassword, loginService.lookupLogin, loginService.logEmployee);
module.exports = router;
server/controllers/login.controller.js
const postgres = require('../../lib/postgres');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
exports.logEmployee = (req, res) => {
res.status(200).json({ token: 'Bearer ' + jwt.sign(req.employee, process.env.SECRET, { expiresIn: 1800 }) });//expires in 1800 seconds
res.end();
};
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 do login', param: req.params.id }] });
}
req.body.kdfResult = derivedKey.toString('hex');
next();
});
};
exports.lookupLogin = (req, res, next) => {
const sql = 'SELECT e.employee_id, e.login FROM employee e WHERE e.login=$1 AND e.password = $2';
postgres.query(sql, [req.body.login, req.body.kdfResult], (err, result) => {
if (err) {
return res.status(500).json({ errors: [{ location: req.path, msg: 'Could not do login', param: req.params.id }] });
}
if (result.rows.length === 0) {
return res.status(404).json({ errors: [{ location: req.path, msg: 'User or password does not match', param: req.params.id }] });
}
req.employee = result.rows[0];
next();
});
};
But you can use the ideas here to use a date validator.
If you need a more complete example, please, let me know.

I'm getting a weird error while using "next-connect" in my nextJS project that had fixed itself in dev but is now even worse in prod

As i said in the title i am using a npm package called "next-connect" to structure my api. Every api route that i created suffered from this error. This is the error :
Unhandled rejection: TypeError: Cannot read property 'end' of undefined
at next (/var/task/node_modules/next-connect/lib/index.js:43:54)
at next (/var/task/node_modules/next-connect/lib/index.js:49:9)
at next (/var/task/node_modules/next-connect/lib/index.js:58:16)
at next (/var/task/node_modules/next-connect/lib/index.js:49:9)
at next (/var/task/node_modules/next-connect/lib/index.js:58:16)
at next (/var/task/node_modules/next-connect/lib/index.js:58:16)
at next (/var/task/node_modules/next-connect/lib/index.js:60:9)
at <anonymous>
at process._tickDomainCallback (internal/process/next_tick.js:228:7)
After a few minutes of trying i get to solve it in dev. Most of this due to messing with the .env file.
Here the code from my API Route :
import nextConnect from "next-connect";
import bcrypt from "bcryptjs";
import middleware from "../../middlewares/middleware";
const handler = nextConnect();
handler.use(middleware);
handler.get((req, res) => {
if (req.user) {
const { name, email, bio, profilePicture, emailVerified } = req.user;
return res.status(200).send({
status: "ok",
data: {
isLoggedIn: true,
user: {
name,
email,
bio,
profilePicture,
emailVerified
}
}
});
}
return res.status(200).send({
status: "ok",
data: {
isLoggedIn: false,
user: {}
}
});
});
handler.post((req, res) => {
const { email, password } = req.body;
return req.db
.collection("users")
.findOne({ email })
.then(user => {
if (user) {
return bcrypt.compare(password, user.password).then(result => {
if (result) return Promise.resolve(user);
return Promise.reject(Error("The password you entered is incorrect"));
});
}
return Promise.reject(Error("The email does not exist"));
})
.then(user => {
req.session.userId = user._id;
return res.send({
status: "ok",
message: `Welcome back, ${user.name}!`
});
})
.catch(error =>
res.send({
status: "error",
message: error.toString()
})
);
});
handler.delete((req, res) => {
delete req.session.userId;
return res.status(200).send({
status: "ok",
message: "You have been logged out."
});
});
export default handler;
And here code from the next-connect package (the one mentioned in the error report) :
module.exports = () => {
function connect(req, res) {
connect.handle(req, res);
}
connect.stack = [];
function add(method, ...handle) {
for (let i = 0; i < handle.length; i += 1) {
if (handle[i].stack) Object.assign(this.stack, handle[i].stack);
else this.stack.push({ handle: handle[i], method });
}
}
// method routing
connect.get = add.bind(connect, 'GET');
connect.head = add.bind(connect, 'HEAD');
connect.post = add.bind(connect, 'POST');
connect.put = add.bind(connect, 'PUT');
connect.delete = add.bind(connect, 'DELETE');
connect.options = add.bind(connect, 'OPTIONS');
connect.trace = add.bind(connect, 'TRACE');
connect.patch = add.bind(connect, 'PATCH');
// middleware
connect.use = add.bind(connect, '');
connect.error = add.bind(connect, 'ERR');
connect.apply = function apply(req, res) {
return new Promise((resolve) => this.handle(req, res, resolve));
};
connect.handle = function handle(req, res, done) {
let idx = 0;
const { stack } = this;
async function next(err) {
const layer = stack[idx];
idx += 1;
// all done
if (!layer) {
if (done) done();
else if (!res.headersSent) res.writeHead(404).end();
return;
}
// check if is correct method or middleware
if (layer.method !== '' && layer.method !== 'ERR' && layer.method !== req.method) {
next(err);
return;
}
try {
if (!err) { await layer.handle(req, res, next); return; }
// there is an error
if (layer.method === 'ERR' || layer.handle.length === 4) {
await layer.handle(err, req, res, next);
} else next(err);
} catch (error) {
next(error);
}
}
// Init stack chain
next();
};
return connect;
};

Return middleware from middleware

I am using express-validator and would like to have different checks based on a value in the request body.
I have created a function for this, but I am not getting any responses back (i.e. express just hangs.):
validation/profile.js
module.exports = function (req,res,next) {
if (req.body.type == 'teacher') {
return check('name').exists().withMessage('Name is required'),
} else {
return check('student_id').exists().withMessage('Student id is required'),
}
}
app.js
router.put('/', require('./validation/profile'), (req, res, next) => {
const errors = validationResult(req).formatWith(errorFormatter)
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.mapped() })
} else {
res.send(req.user)
}
})
If however, I write my function as a normal function (not as middleware with 3 params) and call it, it all works. But this way, I won't have access to the request object. I have to "hard-code" the params.
validation/profile.js
module.exports = function (type) {
if (type == 'teacher') {
return check('name').exists().withMessage('Name is required'),
} else {
return check('student_id').exists().withMessage('Student id is required'),
}
}
app.js
router.put('/', require('./validation/profile')('teacher'), (req, res, next) => {
const errors = validationResult(req).formatWith(errorFormatter)
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.mapped() })
} else {
res.send(req.user)
}
})
Any suggestions on how could I achieve having different checks based on a value in the request body?
The express-validator check API creates the middleware, you should attach it to express directly or call it yourself as express would.
// Use routers so multiple checks can be attached to them.
const teacherChecks = express.Router();
teacherChecks.use(check('name').exists().withMessage('Name is required'));
const studentChecks = express.Router();
studentChecks .use(check('student_id').exists().withMessage('Student id is required'));
module.exports = function (req,res,next) {
if (req.body.type == 'teacher') {
teacherChecks(req, res, next);
} else {
studentChecks(req, res, next);
}
}
You could also potentially use oneOf to do the same thing.
router.put('/', oneOf([
check('name').exists().withMessage('Name is required'),
check('student_id').exists().withMessage('Student id is required')
], 'Invalid request body'), (req, res, next) => {
const errors = validationResult(req).formatWith(errorFormatter)
if (
!errors.isEmpty()
) {
return res.status(422).json({errors: errors.mapped()})
}
else {
res.send(req.user)
}
});

How can I extend the res object in express and use the extended property in a error middleware?

Here's the response-hander.js middleware file. I have 2 function that extend the res object normal behavior. If I use these function in a normal route everything works fine, the problem shows when I try to call one of these in another middleware.
var responseHandler;
responseHandler = function(req, res, next) {
res.jsonSuccess = function(results) {
res.status(200).json({
status: "OK",
results: results
});
};
res.jsonBadRequest = function(message) {
res.status(400).json({
status: "BAD_REQUEST",
error_message: message
});
};
next();
};
module.exports = responseHandler;
Here's the error-handler.js middleware:
var errorHandler;
errorHandler = function(error, req, res, next){
if (error instanceof Error) {
res.jsonBadRequest("Error.");
}
}
module.exports = errorHandler;
The error:
TypeError: res.jsonBadRequest is not a function
Example:
This works
app.get("/my/route", function(req, res) {
return res.jsonBadRequest("Test");
});
This doesn't
app.get("/my/route", function(req, res) {
// the error middleware gets calld but dosn't find my function.
throw new Error();
});

Resources