Just A Guy
Just A Guy

Reputation: 159

Keep getting "Can't set headers after they are sent" using Node/Express

I keep getting "Can't set headers after they are sent" building a Node/Express API.

The issue is I am not setting the headers after the response has been sent anywhere. I am always calling res.status(xxx).json({}) to close ever condition.

Route

const router = require('express').Router();
router.get('/password/validate/:hash', PasswordController.validate);
router.post('/password/update', PasswordController.update);

Controller

This is where the error is occurring. I am calling the validate request specifically.

// Import node packages
const mongoose = require('mongoose');
const Password = require('../models/password');
const User = require('../models/user');
const bcrypt = require('bcryptjs');
const moment = require('moment');
const string = require('../middleware/string_functions')

exports.update = (req, res, next) => {
    User.findOne({ email: req.body.email })
        .exec()
        .then(user => {
            if (!user) {
                res.status(401).json({
                    message: 'Cannot retrieve account'
                })
            }
            const expiry = moment().add(30, 'seconds');
            const unique_string = string.generate_random(32);
            const url_hash = string.base64_encode(unique_string +':'+ user._id);
            bcrypt.hash(unique_string, 10, (err, hash) => {
                if (err) {
                    res.status(500).json({ 
                        error: err.message
                    })
                }
                const query = { user_id: user._id }
                const newData = {
                    hash,
                    expiry
                }
                Password.findOneAndUpdate(query, newData, { upsert: true, new: true })
                    .exec()
                    .then(request => {
                        res.status(201).json({
                            message: 'success',
                            url: 'localhost:8081/users/password/validate/' + url_hash,
                            data: request
                        })
                    })
                    .catch(err => {
                        res.status(500).json({
                            error: err.message
                        })
                    })
            })
        })
        .catch(err => {
            res.status(500).json({
                error: err.message
            })
        })
}

exports.validate = (req, res, next) => {
    if (!req.params.hash) {
        res.status(500).json({
            error: 'Missing hash'
        })
    }
    const data = string.base64_decode(req.params.hash).split(':');
    console.log(data)
    Password.findOne({ user_id: data[1] })
        .exec()
        .then(request => {
            if (!request) {
                res.status(404).json({
                    message: 'Change request not found or expired'
                })
            }
            bcrypt.compare( data[0], request.hash, (err, result) => {
                if (err) {
                    res.status(500).json({
                        error: err.message
                    })
                }
                if (result) {
                    if (moment().isAfter(request.expiry)) {
                        res.status(401).json({
                            message: 'Time has expired'
                        })
                    }
                    res.status(200).json({
                        message: 'Hash validation successful'
                    })
                }
                res.status(500).json({
                    error: 'Something went wrong'
                })
            })
        })
        .catch(err => {
            res.status(500).json({
                error: err.message
            })
        })
}

Console Error

_http_outgoing.js:494
    throw new Error('Can\'t set headers after they are sent.');
    ^

Error: Can't set headers after they are sent.
    at validateHeader (_http_outgoing.js:494:11)
    at ServerResponse.setHeader (_http_outgoing.js:501:3)
    at ServerResponse.header (/Users/chrislloyd/Development/Projects/happy-hour-api/node_modules/express/lib/response.js:767:10)
    at ServerResponse.send (/Users/chrislloyd/Development/Projects/happy-hour-api/node_modules/express/lib/response.js:170:12)
    at ServerResponse.json (/Users/chrislloyd/Development/Projects/happy-hour-api/node_modules/express/lib/response.js:267:15)
    at bcrypt.compare (/Users/chrislloyd/Development/Projects/happy-hour-api/api/controllers/passwords.js:83:22)
    at /Users/chrislloyd/Development/Projects/happy-hour-api/node_modules/bcryptjs/dist/bcrypt.js:297:21
    at /Users/chrislloyd/Development/Projects/happy-hour-api/node_modules/bcryptjs/dist/bcrypt.js:1353:21
    at Immediate.next [as _onImmediate] (/Users/chrislloyd/Development/Projects/happy-hour-api/node_modules/bcryptjs/dist/bcrypt.js:1233:21)
    at runCallback (timers.js:789:20)
    at tryOnImmediate (timers.js:751:5)
    at processImmediate [as _immediateCallback] (timers.js:722:5)

Updated Example

exports.update = (req, res, next) => {
    // Check if hash value exists
    if (!req.params.hash) {
        res.status(500).json({
            error: 'Missing hash value'
        });
        return;
    }
    // Check if password and confirmation are the same
    if (req.body.password != req.body.passwordConfirmation) {
        res.status(401).json({
            message: 'Password confirmation does not match'
        });
        return;
    }
    // Decode and split hash and user id into array
    const data = string.base64_decode(req.params.hash).split(':');
    // Find record that contains user id
    Password.findOne({ user_id: data[1] })
        .exec()
        .then(request => {
            console.log(request)
            // Throw 404 error if record is not found
            if (!request) {
                return res.status(404).json({
                    message: 'Password change request doest not exist or timed out'
                });
            }
            // Check if change request has expired
            if (moment().isAfter(request.expiry)) {
                res.status(401).json({
                    message: 'Password change request expired',
                    request: {
                        request: 'http://localhost:3001/users/password/request'
                    }
                });
                // Delete expired record
                Password.remove({ _id: request._id })
                    .exec()
                    .catch(err => {
                        res.status(500).json({
                            error: err.message
                        });
                    });
                return;
            }
            // Compare hash value from encoded string to encrypted hash value in database
            console.log(mongoose.Types.ObjectId(request.user_id))
            bcrypt.compare( data[0], request.hash, (err, result) => {
                // Bcrypt error performing comparison
                if (err) {
                    res.status(500).json({
                        error: err.message
                    });
                    return;
                }
                // Check if result is true
                if (result) {
                    // Find user record matching request.user_id and update password
                    User.findOneAndUpdate({ _id: mongoose.Types.ObjectId(request.user_id) }, {$set: { password: req.body.password  }}, {new: true}, (err, user) => {
                        console.log(user)
                        // Error finding and updating user record
                        if (err) {
                            res.status(500).json({
                                error: err.message
                            });
                            return;
                        }
                        // If returned user account is not null
                        if (user) {
                            res.status(200).json({
                                message: 'Password updated',
                                user
                            });
                            return;
                        }
                        // Could not find user record
                        res.status(404).json({
                            message: 'Could not find user account to update'
                        });
                        return;
                    })
                }
                // Catch all error
                res.status(500).json({
                    error: 'Something went wrong'
                });
                return;
            })
        })
        .catch(err => {
            res.status(500).json({
                error: err.message
            });
            return;
        });
}

Upvotes: 2

Views: 4150

Answers (2)

jfriend00
jfriend00

Reputation: 707298

That particular error is caused when you send multiple responses to the same request.

You see to be thinking that as soon as you do res.status(...).json(...) that your function returns and stops executing. It does not. res.json() is just a regular function call. It doesn't change the control flow in your function at all (unless it throws an exception). A successful call to res.json() executes and then your function just keeps right on executing the lines of code that follow.

What you need is a return statement after each time you send a response (if there is any other code in your function that could execute and send another response) so that your function doesn't continue to execute and send another response or you could bracket your responses in if/else statements so you don't execute the sending of more than one response.

Here's a fixed version with 5 added return statements to keep the rest of your code from executing after you've sent a response and to keep you from sending multiple responses to the same request. Each addition is commented with ==> added:

// Import node packages
const mongoose = require('mongoose');
const Password = require('../models/password');
const User = require('../models/user');
const bcrypt = require('bcryptjs');
const moment = require('moment');
const string = require('../middleware/string_functions')

exports.update = (req, res, next) => {
    User.findOne({ email: req.body.email })
        .exec()
        .then(user => {
            if (!user) {
                res.status(401).json({
                    message: 'Cannot retrieve account'
                })
                return;            // <== added
            }
            const expiry = moment().add(30, 'seconds');
            const unique_string = string.generate_random(32);
            const url_hash = string.base64_encode(unique_string +':'+ user._id);
            bcrypt.hash(unique_string, 10, (err, hash) => {
                if (err) {
                    res.status(500).json({ 
                        error: err.message
                    })
                    return;            // <== added
                }
                const query = { user_id: user._id }
                const newData = {
                    hash,
                    expiry
                }
                Password.findOneAndUpdate(query, newData, { upsert: true, new: true })
                    .exec()
                    .then(request => {
                        res.status(201).json({
                            message: 'success',
                            url: 'localhost:8081/users/password/validate/' + url_hash,
                            data: request
                        })
                    })
                    .catch(err => {
                        res.status(500).json({
                            error: err.message
                        })
                    })
            })
        })
        .catch(err => {
            res.status(500).json({
                error: err.message
            })
        })
}

exports.validate = (req, res, next) => {
    if (!req.params.hash) {
        res.status(500).json({
            error: 'Missing hash'
        })
    }
    const data = string.base64_decode(req.params.hash).split(':');
    console.log(data)
    Password.findOne({ user_id: data[1] })
        .exec()
        .then(request => {
            if (!request) {
                res.status(404).json({
                    message: 'Change request not found or expired'
                })
                return;            // <== added
            }
            bcrypt.compare( data[0], request.hash, (err, result) => {
                if (err) {
                    res.status(500).json({
                        error: err.message
                    })
                    return;            // <== added
                }
                if (result) {
                    if (moment().isAfter(request.expiry)) {
                        res.status(401).json({
                            message: 'Time has expired'
                        })
                    }
                    res.status(200).json({
                        message: 'Hash validation successful'
                    })
                    return;            // <== added
                }
                res.status(500).json({
                    error: 'Something went wrong'
                })
            })
        })
        .catch(err => {
            res.status(500).json({
                error: err.message
            })
        })
}

Upvotes: 7

The res object by itself does not stop the execution of your program. You must use return if you prefer to use Guard Clauses instead of Nested Conditions

Replace statements like this:

if (err) {
  res.status(500).json({
    error: err.message
  })
}

With this:

if (err) {
  res.status(500).json({
    error: err.message
  });
  return; // return statement added
}

Upvotes: 3

Related Questions