necroface
necroface

Reputation: 3465

Warning - Unhandled promise rejection: Can't set headers after they are sent

I have a route handler like this:

router.route('/sipprovider/:sipprovider_id/phonelist')
        .post((req, res) => {
            const body = req.body;
            if (typeof body.phones === 'undefined') {
                return res.status(400).json({
                    success: false,
                    error: { message: 'Invalid input' }
                });
            }
            if (!Array.isArray(body.phones) || body.phones.length === 0) {
                return res.status(400).json({
                    success: false,
                    error: { message: 'Invalid input' }
                });
            }
            let i = body.phones.indexOf('');
            while (i >= 0) {
                body.phones.splice(i, 1);
                i = body.phones.indexOf('');
            }
            body.phones.map((phone) => {
                if (typeof phone !== 'string') {
                    return res.status(400).json({
                        success: false,
                        error: { message: 'Invalid input' }
                    });
                }
            });
            const SipProvider = new SipProviderModel(db, logger);
            SipProvider.pushPhoneList(req.params.sipprovider_id, body.phones)
                .then((result) => {
                    if (result) {
                        return res.json({ success: true });
                    }
                    return res.status(404).json({
                        success: false,
                        error: { message: 'Sip provider not found' }
                    });
                })
                .catch((error) => res.status(400).json({
                    success: false,
                    error: { message: error }
                }));
        });

pushPhoneList function of SipProvider is a promise. And I write several tests:

describe('POST', () => {
            let id;
            beforeEach(() => SipProvider.create(data).then((result) => id = result._id));
            it('Should return 200 if successful', (done) => {
                chai.request(app)
                    .post('/sipprovider/' + id + '/phonelist')
                    .set('Authorization', token)
                    .send({ phones: ['02873000050'] })
                    .end((err, res) => {
                        expect(res.status).to.equal(200);
                        expect(res.body.success).to.equal(true);
                        return done();
                    });
            });
            it('Should return 200 if successful', (done) => {
                chai.request(app)
                    .post('/sipprovider/' + id + '/phonelist')
                    .set('Authorization', token)
                    .send({ phones: ['02873000050', ''] })
                    .end((err, res) => {
                        expect(res.status).to.equal(200);
                        expect(res.body.success).to.equal(true);
                        return done();
                    });
            });
            it('Should return 400 if input is invalid (undefined phones)', (done) => {
                chai.request(app)
                    .post('/sipprovider/' + id + '/phonelist')
                    .set('Authorization', token)
                    .end((err, res) => {
                        expect(res.status).to.equal(400);
                        expect(res.body.success).to.equal(false);
                        expect(res.body.error.message).to.equal('Invalid input');
                        return done();
                    });
            });
            it('Should return 400 if input is invalid (not an array)', (done) => {
                chai.request(app)
                    .post('/sipprovider/' + id + '/phonelist')
                    .set('Authorization', token)
                    .send({ phones: '02873000050' })
                    .end((err, res) => {
                        expect(res.status).to.equal(400);
                        expect(res.body.success).to.equal(false);
                        expect(res.body.error.message).to.equal('Invalid input');
                        return done();
                    });
            });
            it('Should return 400 if input is invalid (empty list)', (done) => {
                chai.request(app)
                    .post('/sipprovider/' + id + '/phonelist')
                    .set('Authorization', token)
                    .send({ phones: [] })
                    .end((err, res) => {
                        expect(res.status).to.equal(400);
                        expect(res.body.success).to.equal(false);
                        expect(res.body.error.message).to.equal('Invalid input');
                        return done();
                    });
            });
            it('Should return 400 if input is invalid (not an array of string)', (done) => {
                chai.request(app)
                    .post('/sipprovider/' + id + '/phonelist')
                    .set('Authorization', token)
                    .send({ phones: ['02873000050', {}] })
                    .end((err, res) => {
                        expect(res.status).to.equal(400);
                        expect(res.body.success).to.equal(false);
                        expect(res.body.error.message).to.equal('Invalid input');
                        return done();
                    });
            });
            it('Should return 404 if not found', (done) => {
                SipProvider.delete(id).then(
                    () => {
                        chai.request(app)
                            .post('/sipprovider/' + id + '/phonelist')
                            .set('Authorization', token)
                            .send({ phones: ['02873000050'] })
                            .end((err, res) => {
                                expect(res.status).to.equal(404);
                                expect(res.body.success).to.equal(false);
                                expect(res.body.error.message).to.equal('Sip provider not found');
                                return done();
                            });
                    });
            });
            afterEach(() => SipProvider.delete(id));
        });

All of them pass, but the logger records some warnings in the last test containing UnhandledPromiseRejectionWarning: Unhandled promise rejection, Can't set headers after they are sent

I haven't got why the ultimate catch block in route handler gets called. I thought only when the promise function gets rejected, catch will occur

Upvotes: 0

Views: 319

Answers (1)

jfriend00
jfriend00

Reputation: 707318

First off, in this code:

        body.phones.map((phone) => {
            if (typeof phone !== 'string') {
                return res.status(400).json({
                    success: false,
                    error: { message: 'Invalid input' }
                });
            }
        });

You can end up calling res.status() multiple times. That can easily cause the "Can't set headers after they are sent" problem. The problem here is that return only returns from the .map() callback. It doesn't stop other callbacks in .map() and it doesn't prevent the rest of your code after that block from executing which will also cause another response to be sent.

You could fix that issue by doing something like this:

if (!body.phones.every(phone => typeof phone === 'string')) {
     return res.status(400).json({
         success: false,
         error: { message: 'Invalid input' }
     });
}

This will run through all the body.phones items, test to see if they are all the correct type and then, outside the loop, if they are not all the correct type, then it will return. This will only send one response and will return from the outer function, stopping further execution.

Upvotes: 3

Related Questions