1
votes

This has been driving me crazy.. The app I'm working on is throwing the "Cannot set headers after they are sent to the client" error. I'm very new to writing anything on the back-end, and can't figure out why this is being thrown. I've read through all of the related posts on the issue and STILL can't see what's wrong. Please help me find what I'm missing.

This is the route I'm trying to access, userRoutes.js:

router.post('/login', (req, res, next) => {
  console.log('1');
  let fetchedUser;
  HaulerUser.findOne({ username: req.body.username })
    .then(user => {
      console.log('2');
      if (!user) {
        return res.status(401).json({
          message: 'Auth failed'
        });
      }
      console.log('3');
      fetchedUser = user;
      return bcrypt.compare(req.body.password, user.password);
    })
    .then(result => {
      console.log('4');
      if(!result) {
        return res.status(401).json({
          message: 'Auth failed'
        });
      }
      const token = jwt.sign(
        { username: fetchedUser.username, userId: fetchedUser._id },
        'secret_this_should_be_longer',
        { expiresIn: '1hr' }
      );
      console.log('5');
      res.status(200).json({
        token: token,
        expiresIn: 3600,
        userId: fetchedUser._id
      });
      console.log('6');
    })
    .catch(err => {
      console.log('7');
      return res.status(401).json({
        message: 'Invalid authentication credentials',
        error: err
      });
    });
});

Also, it's only printing 1, 2, 4, and 7 to the console????

This is the main NodeJS code.

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader(
    'Access-Control-Allow-Headers',
    'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  res.setHeader(
    'Access-Control-Allow-Methods',
    'GET, POST, PATCH, PUT, DELETE, OPTIONS'
  );
  next();
});

app.use('/api/user', userRoutes);
app.use('/api/manifests', manifestsRoutes);
app.use('/api/sites', sitesRoutes);

I'm a bit over my head with this one... I'm sure it's something small... please help.....

1
Because you execute res.status().json() in every callbacks, and these callbacks get executed one after another - kkkkkkk
They're all in 'if' blocks though. So only one of them gets executed, right?.. - camstar915
Hey, you cannot send headers for response twice to the client. You can send the headers only once. So, try and send your whole output or headers after all callbacks are executed or you can make a whole new Call back after all the callbacks are executed and create your response there! Cheers :) - Raja Shekar
I don't think so. 1, 2, 4, 7 got printed means that res.status(401).json() gets executed 3 times - kkkkkkk

1 Answers

1
votes

As the comments have mentioned, you are calling res.json() more than once, which is what the error is saying.

You're running into a common issue that folks hit when learning how to chain Promises. Returning res.status(401).json({...}) does not reject the Promise and instead returns the result of res.status(401).json({...}) to the next .then() block which in turn does the same down the stack when the user is not found.

You have a few options to fix this but the most common way would be to use Exceptions to exit out of the Promise chain. Instead of res.status(401).json({...}) in each if block, do throw new Error('{an error message}'). This will exit the chain and jump to your .catch() which sends then calls res.status(401).json({...}). You can then also log the specific error message while returning the generic message to the caller.

router.post('/login', (req, res, next) => {
  console.log('1');
  let fetchedUser;
  HaulerUser.findOne({ username: req.body.username })
    .then(user => {
      console.log('2');
      if (!user) {
        throw new Error(`User '${req.body.username}' not found`);
      }
      console.log('3');
      fetchedUser = user;
      return bcrypt.compare(req.body.password, user.password);
    })
    .then(result => {
      console.log('4');
      if(!result) {
        throw new Error(`Password did not match`);
      }
      const token = jwt.sign(
        { username: fetchedUser.username, userId: fetchedUser._id },
        'secret_this_should_be_longer',
        { expiresIn: '1hr' }
      );
      console.log('5');
      res.status(200).json({
        token: token,
        expiresIn: 3600,
        userId: fetchedUser._id
      });
      console.log('6');
    })
    .catch(err => {
      console.log('7');
      console.log(err);
      res.status(401).json({ // You don't need a return here
        message: 'Invalid authentication credentials',
        error: err
      });
    });
});

If you are on Node 8 or higher, you could use async/await instead of .then()/.catch() and your code would behave how you expected.