'Cannot set headers after they are sent to the client with express-validator, express, mongoose and Next.js

I am building a login/registration form using express-validator and mongoose in next.js.

Heard the best practice was to sanitize your data on the front and backend.

I have some validations on the frontend (i.e. checking if an email via Regex and making sure a password in a particular length).

But now I'd like to use Custom validator to check if a email exists in my mongodb database.

    .post(body('username').custom(value => {
        UserModel.findOne({ 'email': value }).then(user => {
            if (user) {
                return Promise.reject('E-mail already in use');
            }
        });
    }), 

This is the rest of my code:

var router = require('express').Router()
var UserModel = require('../models/UserModel')
var { body } = require('express-validator');

router
    .route('/registration')
    .get(function(req, res) {
        UserModel.find({}, (err, users) => {
            if (err) res.status(500).send(err)
            res.json(users)
        })
    })
    .post(body('username').custom(value => {
        UserModel.findOne({ 'email': value }).then(user => {
            if (user) {
                return Promise.reject('E-mail already in use');
            }
        });
    }), async(req, res, next) => {

        try {
            let newUser = new UserModel(req.body)

            let savedUser = await newUser.save(err => {
                if (err) return res.json({ success: false, error: err })
                return res.json({ success: true })
            })

            if (savedUser) return res.redirect('/users/registration?success=true');
            return next(new Error('Failed to save user for unknown reasons'))

        } catch (err) {
            return next(err)
        }

    })

module.exports = router

And this is the error I'm getting:

Error: Failed to save user for unknown reasons
    at router.route.get.post (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/server/users/index.js:34:25)
    at process._tickCallback (internal/process/next_tick.js:68:7)
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at ServerResponse.setHeader (_http_outgoing.js:470:11)
    at ServerResponse.header (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/response.js:767:10)
    at ServerResponse.send (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/response.js:170:12)
    at ServerResponse.json (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/response.js:267:15)
    at /Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/server/index.js:108:17
    at Layer.handle_error (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/layer.js:71:5)
    at trim_prefix (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:315:13)
    at /Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:284:7
    at Function.process_params (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:335:12)
    at next (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:275:10)
    at Layer.handle_error (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/layer.js:73:5)
    at trim_prefix (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:315:13)
    at /Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:284:7
    at Function.process_params (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:335:12)
    at Immediate.next (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:275:10)
    at Immediate.<anonymous> (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:635:15)
    at runCallback (timers.js:706:11)
    at tryOnImmediate (timers.js:676:5)
    at processImmediate (timers.js:658:5)

Also do I even need this when Mongoose provides when designing models/schema?

var mongoose = require('mongoose')
var emailValidator = require('email-validator')
var bcrypt = require('bcrypt') // hashing function dedicated for passwords

const SALT_ROUNDS = 12

var UserSchema = new mongoose.Schema(
  {
    username_email: {
      type: String,
      required: true,
      lowercase: true,
      index: { unique: true }, // I mean this!
      validate: {
        validator: emailValidator.validate,
        message: props => `${props.value} is not a valid email address`
      }
    },
    password: {
      type: String,
      required: true,
      trim: true,
      index: { unique: true },
      minlength: 8
    }
  },
  {
    timestamps: true
  }
)

UserSchema.pre('save', async function preSave(next) {
  var user = this
  var hash
  if (!user.isModified('password')) return next()
  try {
    hash = await bcrypt.hash(user.password, SALT_ROUNDS)
    user.password = hash
    return next()
  } catch (err) {
    return next(err)
  }
})

UserSchema.methods.comparePassword = async function comparePassword(candidate) {
  return bcrypt.compare(candidate, this.password)
};

module.exports = mongoose.model('User', UserSchema)

And if I don't does that mean checking if the email exists should be moved to the frontend? And If that's the case how would I approach that?

UPDATE

I tried Nick's suggestion but not sure why I'm still getting

`Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

` These are the updated routes:

router
    .route('/registration')
    .get(function(req, res) {
        console.log(0)
        UserModel.find({}, (err, users) => {
            console.log(1)
            if (err) res.status(500).send(err)
            console.log(2)
            return res.json(users)
            console.log(3)
        })
    })
    .post(body('email').custom(value => {
        console.log(4)
        UserModel.findOne({ 'email': value }).then(user => {
            console.log(5)
            if (user) {
                console.log(6)
                return Promise.reject('E-mail already in use');
            }
        });
    }), async(req, res, next) => {
        console.log(7)
        try {
            let newUser = new UserModel(req.body)

            let savedUser = await newUser.save(err => {
                if (err) return res.json({ success: false, error: err })
                console.log(8)
                return res.json({ success: true })
            })
            console.log(9)
            if (savedUser) return res.redirect('/users/registration?success=true');
            console.log("savedUser ", savedUser);
            console.log(10)
            return next(new Error('Failed to save user for unknown reasons'))

        } catch (err) {
            return next(err)
        }

    })

Note that pages will be compiled when you first load them.
GET /_next/static/webpack/d691821e71bf01c860e6.hot-update.json 404 299.194 ms - 1862
GET /_next/static/webpack/42c7a9cb77dec12fc8a3.hot-update.json 200 40.276 ms - 35
4
7
9
savedUser  undefined
10
POST /users/registration 200 21.490 ms - 422
Error: Failed to save user for unknown reasons
    at router.route.get.post (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/server/users/index.js:42:25)
    at process._tickCallback (internal/process/next_tick.js:68:7)
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at ServerResponse.setHeader (_http_outgoing.js:470:11)
    at ServerResponse.header (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/response.js:767:10)
    at ServerResponse.send (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/response.js:170:12)
    at ServerResponse.json (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/response.js:267:15)
    at /Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/server/index.js:108:17
    at Layer.handle_error (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/layer.js:71:5)
    at trim_prefix (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:315:13)
    at /Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:284:7
    at Function.process_params (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:335:12)
    at next (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:275:10)
    at Layer.handle_error (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/layer.js:73:5)
    at trim_prefix (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:315:13)
    at /Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:284:7
    at Function.process_params (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:335:12)
    at Immediate.next (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:275:10)
    at Immediate.<anonymous> (/Users/antoniopavicevac-ortiz/Dropbox/developer_folder/hillfinder/node_modules/express/lib/router/index.js:635:15)
    at runCallback (timers.js:706:11)
    at tryOnImmediate (timers.js:676:5)
    at processImmediate (timers.js:658:5)
5
6
(node:68936) UnhandledPromiseRejectionWarning: E-mail already in use
(node:68936) 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: 2)
(node:68936) [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.
^C


Solution 1:[1]

The error 'Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client' means that res.send/json/redirect fired more than once during the request. Just at a first glance, this code block has the potential of causing this error.

.get(function(req, res) {
        UserModel.find({}, (err, users) => {
            if (err) res.status(500).send(err)
            res.json(users)
        })
    })

Without having looked on the exact request being made, I recommend putting console.log lines everywhere to see if you can pinpoint what happens during runtime.

UPDATE

 .get(function(req, res) {
            UserModel.find({}, (err, users) => {
                // If there's an error, this will fire and continue
                if (err) res.status(500).send(err) 

                // This fires next even if there is an error
                res.json(users)
            })
        })

This is a possibility of what can be causing that issue. There is a potential of res firing twice. The fix would be to add return. This ensures that the code does not continue.

 .get(function(req, res) {
            UserModel.find({}, (err, users) => {                    
                if (err) return res.status(500).send(err)
                return res.json(users)
            })
        })

If this doesn't fix it, I recommend putting console.log statements after all arguments everywhere so you can pinpoint which line it's failing on. For Example:

router
    .route('/registration')
    .get(function(req, res) {
        console.log(0)
        UserModel.find({}, (err, users) => {
          console.log(1)
          if (err) res.status(500).send(err)
          console.log(2)
          res.json(users)
          console.log(3)
        })
    })
    .post(body('username').custom(value => {
        console.log(4)
        UserModel.findOne({ 'email': value }).then(user => {
            console.log(5)
            if (user) {
                console.log(6)
                return Promise.reject('E-mail already in use');
            }
        });
    }), async(req, res, next) => {
        console.log(7)
        try {
            let newUser = new UserModel(req.body)

            let savedUser = await newUser.save(err => {
                if (err) return res.json({ success: false, error: err })
                console.log(8)
                return res.json({ success: true })
            })
            console.log(9)
            if (savedUser) return res.redirect('/users/registration?success=true');
            console.log(10)
            return next(new Error('Failed to save user for unknown reasons'))

        } catch (err) {
            return next(err)
        }

    })

UPDATE 2

So I finally put the code in my editor and noticed that you didn't return your promise in your validator, which may be the reason why you're running into everything async. I added the comment to show you where I returned the promise. Hopefully, this will work for you now :)

router.route('/registration')
    .get(function(req, res) {
        UserModel.find({}, (err, users) => {
            if (err) res.status(500).send(err)
            res.json(users)
        })
    })
    .post(body('username').custom(value => {      
        return UserModel.findOne({ 'email': value }).then(user => { // Return Promise
          if (user) {
              return Promise.reject('E-mail already in use');
          }
        });
    }), async(req, res, next) => {
        try {
            let newUser = new UserModel(req.body)
            let savedUser = await newUser.save(err => {
              if (err) 
                return res.json({ success: false, error: err })
              return res.json({ success: true })
            })

            if (savedUser)
              return res.redirect('/users/registration?success=true');

            return next(new Error('Failed to save user for unknown reasons'))
        } catch (err) {
            return next(err)
        }
    })

module.exports = router

Solution 2:[2]

Explictly return your API responses. So instead of simply saying res.json(users), change it to return res.json(users).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 w. Patrick Gale