// Copyright IBM Corp. 2016,2018. All Rights Reserved. // Node module: strong-error-handler // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT 'use strict'; const cloneAllProperties = require('../lib/clone.js'); const debug = require('debug')('test'); const expect = require('chai').expect; const express = require('express'); const strongErrorHandler = require('..'); const supertest = require('supertest'); const util = require('util'); describe('strong-error-handler', function() { before(setupHttpServerAndClient); beforeEach(resetRequestHandler); after(stopHttpServerAndClient); it('sets nosniff header', function(done) { givenErrorHandlerForError(); request.get('/') .expect('X-Content-Type-Options', 'nosniff') .expect(500, done); }); it('handles response headers already sent', function(done) { givenErrorHandlerForError(); const handler = _requestHandler; _requestHandler = function(req, res, next) { res.end('empty'); process.nextTick(function() { handler(req, res, next); }); }; request.get('/').expect(200, 'empty', done); }); context('status code', function() { it('converts non-error "err.status" to 500', function(done) { givenErrorHandlerForError(new ErrorWithProps({status: 200})); request.get('/').expect(500, done); }); it('converts non-error "err.statusCode" to 500', function(done) { givenErrorHandlerForError(new ErrorWithProps({statusCode: 200})); request.get('/').expect(500, done); }); it('uses the value from "err.status"', function(done) { givenErrorHandlerForError(new ErrorWithProps({status: 404})); request.get('/').expect(404, done); }); it('uses the value from "err.statusCode"', function(done) { givenErrorHandlerForError(new ErrorWithProps({statusCode: 404})); request.get('/').expect(404, done); }); it('prefers "err.statusCode" over "err.status"', function(done) { givenErrorHandlerForError(new ErrorWithProps({ statusCode: 400, status: 404, })); request.get('/').expect(400, done); }); it('handles error from `res.statusCode`', function(done) { givenErrorHandlerForError(); const handler = _requestHandler; _requestHandler = function(req, res, next) { res.statusCode = 507; handler(req, res, next); }; request.get('/').expect( 507, {error: {statusCode: 507, message: 'Insufficient Storage'}}, done ); }); }); context('logging', function() { let logs; beforeEach(redirectConsoleError); afterEach(restoreConsoleError); it('logs by default', function(done) { givenErrorHandlerForError(new Error(), { // explicitly set to undefined to prevent givenErrorHandlerForError // from disabling this option log: undefined, }); request.get('/').end(function(err) { if (err) return done(err); expect(logs).to.have.length(1); done(); }); }); it('honours options.log=false', function(done) { givenErrorHandlerForError(new Error(), {log: false}); request.get('/api').end(function(err) { if (err) return done(err); expect(logs).to.have.length(0); done(); }); }); it('honours options.log=true', function(done) { givenErrorHandlerForError(new Error(), {log: true}); request.get('/api').end(function(err) { if (err) return done(err); expect(logs).to.have.length(1); done(); }); }); it('includes relevant information in the log message', function(done) { givenErrorHandlerForError(new TypeError('ERROR-NAME'), {log: true}); request.get('/api').end(function(err) { if (err) return done(err); const msg = logs[0]; // the request method expect(msg).to.contain('GET'); // the request path expect(msg).to.contain('/api'); // the error name & message expect(msg).to.contain('TypeError: ERROR-NAME'); // the stack expect(msg).to.contain(__filename); done(); }); }); it('handles array argument', function(done) { givenErrorHandlerForError( [new TypeError('ERR1'), new Error('ERR2')], {log: true} ); request.get('/api').end(function(err) { if (err) return done(err); const msg = logs[0]; // the request method expect(msg).to.contain('GET'); // the request path expect(msg).to.contain('/api'); // the error name & message for all errors expect(msg).to.contain('TypeError: ERR1'); expect(msg).to.contain('Error: ERR2'); // verify that stacks are included too expect(msg).to.contain(__filename); done(); }); }); it('handles non-Error argument', function(done) { givenErrorHandlerForError('STRING ERROR', {log: true}); request.get('/').end(function(err) { if (err) return done(err); const msg = logs[0]; expect(msg).to.contain('STRING ERROR'); done(); }); }); const _consoleError = console.error; function redirectConsoleError() { logs = []; console.error = function() { const msg = util.format.apply(util, arguments); logs.push(msg); }; } function restoreConsoleError() { console.error = _consoleError; logs = []; } }); context('JSON response', function() { it('contains all error properties when debug=true', function(done) { const error = new ErrorWithProps({ message: 'a test error message', code: 'MACHINE_READABLE_CODE', details: 'some details', extra: 'sensitive data', }); givenErrorHandlerForError(error, {debug: true}); requestJson().end(function(err, res) { if (err) return done(err); const expectedData = { statusCode: 500, message: 'a test error message', name: 'ErrorWithProps', code: 'MACHINE_READABLE_CODE', details: 'some details', extra: 'sensitive data', stack: error.stack, }; expect(res.body).to.have.property('error'); expect(res.body.error).to.eql(expectedData); done(); }); }); it('includes code property for 4xx status codes when debug=false', function(done) { const error = new ErrorWithProps({ statusCode: 400, message: 'error with code', name: 'ErrorWithCode', code: 'MACHINE_READABLE_CODE', }); givenErrorHandlerForError(error, {debug: false}); requestJson().end(function(err, res) { if (err) return done(err); const expectedData = { statusCode: 400, message: 'error with code', name: 'ErrorWithCode', code: 'MACHINE_READABLE_CODE', }; expect(res.body).to.have.property('error'); expect(res.body.error).to.eql(expectedData); done(); }); }); it('excludes code property for 5xx status codes when debug=false', function(done) { const error = new ErrorWithProps({ statusCode: 500, code: 'MACHINE_READABLE_CODE', }); givenErrorHandlerForError(error, {debug: false}); requestJson().end(function(err, res) { if (err) return done(err); const expectedData = { statusCode: 500, message: 'Internal Server Error', }; expect(res.body).to.have.property('error'); expect(res.body.error).to.eql(expectedData); done(); }); }); it('contains non-enumerable Error properties when debug=true', function(done) { const error = new Error('a test error message'); givenErrorHandlerForError(error, {debug: true}); requestJson().end(function(err, res) { if (err) return done(err); expect(res.body).to.have.property('error'); const resError = res.body.error; expect(resError).to.have.property('name', 'Error'); expect(resError).to.have.property('message', 'a test error message'); expect(resError).to.have.property('stack', error.stack); done(); }); }); it('should allow setting safe fields when status=5xx', function(done) { const error = new ErrorWithProps({ name: 'Error', safeField: 'SAFE', unsafeField: 'UNSAFE', }); givenErrorHandlerForError(error, { safeFields: ['safeField'], }); requestJson().end(function(err, res) { if (err) return done(err); expect(res.body).to.have.property('error'); expect(res.body.error).to.have.property('safeField', 'SAFE'); expect(res.body.error).not.to.have.property('unsafeField'); done(); }); }); it('safe fields falls back to existing data', function(done) { const error = new ErrorWithProps({ name: 'Error', isSafe: false, }); givenErrorHandlerForError(error, { safeFields: ['statusCode', 'isSafe'], }); requestJson().end(function(err, res) { if (err) return done(err); expect(res.body.error.statusCode).to.equal(500); expect(res.body.error.isSafe).to.equal(false); done(); }); }); it('should allow setting safe fields when status=4xx', function(done) { const error = new ErrorWithProps({ name: 'Error', statusCode: 422, safeField: 'SAFE', unsafeField: 'UNSAFE', }); givenErrorHandlerForError(error, { safeFields: ['safeField'], }); requestJson().end(function(err, res) { if (err) return done(err); expect(res.body).to.have.property('error'); expect(res.body.error).to.have.property('safeField', 'SAFE'); expect(res.body.error).not.to.have.property('unsafeField'); done(); }); }); it('contains subset of properties when status=4xx', function(done) { const error = new ErrorWithProps({ name: 'ValidationError', message: 'The model instance is not valid.', statusCode: 422, details: 'some details', extra: 'sensitive data', }); givenErrorHandlerForError(error); requestJson().end(function(err, res) { if (err) return done(err); expect(res.body).to.have.property('error'); expect(res.body.error).to.eql({ name: 'ValidationError', message: 'The model instance is not valid.', statusCode: 422, details: 'some details', // notice the property "extra" is not included }); done(); }); }); it('contains only safe info when status=5xx', function(done) { // Mock an error reported by fs.readFile const error = new ErrorWithProps({ name: 'Error', message: 'ENOENT: no such file or directory, open "/etc/passwd"', errno: -2, code: 'ENOENT', syscall: 'open', path: '/etc/password', }); givenErrorHandlerForError(error); requestJson().end(function(err, res) { if (err) return done(err); expect(res.body).to.have.property('error'); expect(res.body.error).to.eql({ statusCode: 500, message: 'Internal Server Error', }); done(); }); }); it('handles array argument as 500 when debug=false', function(done) { const errors = [new Error('ERR1'), new Error('ERR2'), 'ERR STRING']; givenErrorHandlerForError(errors); requestJson().expect(500).end(function(err, res) { if (err) return done(err); const data = res.body.error; expect(data).to.have.property('message').that.match(/multiple errors/); expect(data).to.have.property('details').eql([ {statusCode: 500, message: 'Internal Server Error'}, {statusCode: 500, message: 'Internal Server Error'}, {statusCode: 500, message: 'Internal Server Error'}, ]); done(); }); }); it('returns all array items when debug=true', function(done) { const testError = new ErrorWithProps({ message: 'expected test error', statusCode: 400, }); const anotherError = new ErrorWithProps({ message: 'another expected error', statusCode: 500, }); const errors = [testError, anotherError, 'ERR STRING']; givenErrorHandlerForError(errors, {debug: true}); requestJson().expect(500).end(function(err, res) { if (err) return done(err); const data = res.body.error; expect(data).to.have.property('message').that.match(/multiple errors/); const expectedDetails = [ getExpectedErrorData(testError), getExpectedErrorData(anotherError), {message: 'ERR STRING', statusCode: 500}, ]; expect(data).to.have.property('details').to.eql(expectedDetails); done(); }); }); it('includes safeFields of array items when debug=false', (done) => { const internalError = new ErrorWithProps({ message: 'a test error message', code: 'MACHINE_READABLE_CODE', details: 'some details', extra: 'sensitive data', }); const validationError = new ErrorWithProps({ name: 'ValidationError', message: 'The model instance is not valid.', statusCode: 422, code: 'VALIDATION_ERROR', details: 'some details', extra: 'sensitive data', }); const errors = [internalError, validationError, 'ERR STRING']; givenErrorHandlerForError(errors, { debug: false, safeFields: ['code'], }); requestJson().end(function(err, res) { if (err) return done(err); const data = res.body.error; const expectedInternalError = { statusCode: 500, message: 'Internal Server Error', code: 'MACHINE_READABLE_CODE', // notice the property "extra" is not included }; const expectedValidationError = { statusCode: 422, message: 'The model instance is not valid.', name: 'ValidationError', code: 'VALIDATION_ERROR', details: 'some details', // notice the property "extra" is not included }; const expectedErrorFromString = { message: 'Internal Server Error', statusCode: 500, }; const expectedDetails = [ expectedInternalError, expectedValidationError, expectedErrorFromString, ]; expect(data).to.have.property('message').that.match(/multiple errors/); expect(data).to.have.property('details').to.eql(expectedDetails); done(); }); }); it('handles non-Error argument as 500 when debug=false', function(done) { givenErrorHandlerForError('Error Message', {debug: false}); requestJson().expect(500).end(function(err, res) { if (err) return done(err); expect(res.body.error).to.eql({ statusCode: 500, message: 'Internal Server Error', }); done(); }); }); it('returns non-Error argument in message when debug=true', function(done) { givenErrorHandlerForError('Error Message', {debug: true}); requestJson().expect(500).end(function(err, res) { if (err) return done(err); expect(res.body.error).to.eql({ statusCode: 500, message: 'Error Message', }); done(); }); }); it('handles Error objects containing circular properties', function(done) { const circularObject = {}; circularObject.recursiveProp = circularObject; const error = new ErrorWithProps({ statusCode: 422, message: 'The model instance is not valid.', name: 'ValidationError', code: 'VALIDATION_ERROR', details: circularObject, }); givenErrorHandlerForError(error, {debug: true}); requestJson().end(function(err, res) { if (err) return done(err); expect(res.body).to.have.property('error'); expect(res.body.error).to.have.property('details'); expect(res.body.error.details).to.have.property('recursiveProp', '[Circular]'); done(); }); }); function requestJson(url) { return request.get(url || '/') .set('Accept', 'text/plain') .expect('Content-Type', /^application\/json/); } }); context('HTML response', function() { it('contains all error properties when debug=true', function(done) { const error = new ErrorWithProps({ message: 'a test error message', details: 'some details', extra: 'sensitive data', }); error.statusCode = 500; givenErrorHandlerForError(error, {debug: true}); requestHTML() .expect(500) .expect(/