diff --git a/README.md b/README.md index 3bf7462..50b07ba 100644 --- a/README.md +++ b/README.md @@ -1310,7 +1310,7 @@ api.use(errorHandler1,errorHandler2) ### Error Types -Lambda API provides several different types of errors that can be used by your application. `RouteError`, `MethodError`, `ResponseError`, and `FileError` will all be passed to your error middleware. `ConfigurationError`s will throw an exception when you attempt to `.run()` your route and can be caught in a `try/catch` block. Most error types contain additional properties that further detail the issue. +Lambda API provides several different types of errors that can be used by your application. `ApiError`, `RouteError`, `MethodError`, `ResponseError`, and `FileError` will all be passed to your error middleware. `ConfigurationError`s will throw an exception when you attempt to `.run()` your route and can be caught in a `try/catch` block. Most error types contain additional properties that further detail the issue. ```javascript const errorHandler = (err,req,res,next) => { diff --git a/__tests__/errorHandling.unit.js b/__tests__/errorHandling.unit.js index ee14074..0bbbfcd 100644 --- a/__tests__/errorHandling.unit.js +++ b/__tests__/errorHandling.unit.js @@ -9,11 +9,11 @@ const api = require('../index')({ version: 'v1.0' }) const api2 = require('../index')({ version: 'v1.0' }) const api3 = require('../index')({ version: 'v1.0' }) const api4 = require('../index')({ version: 'v1.0' }) -const api5 = require('../index')({ version: 'v1.0', logger: { access: 'never' }}) +const api5 = require('../index')({ version: 'v1.0', logger: { access: 'never' } }) const api_errors = require('../index')({ version: 'v1.0' }) const api6 = require('../index')() // no props -const api7 = require('../index')({ version: 'v1.0', logger: { errorLogging: false }}) -const api8 = require('../index')({ version: 'v1.0', logger: { access: 'never', errorLogging: true }}) +const api7 = require('../index')({ version: 'v1.0', logger: { errorLogging: false } }) +const api8 = require('../index')({ version: 'v1.0', logger: { access: 'never', errorLogging: true } }) const errors = require('../lib/errors'); // Init API with custom gzip serializer and base64 @@ -24,13 +24,13 @@ const api9 = require('../index')({ 'content-encoding': ['gzip'] }, serializer: body => { - const json = JSON.stringify(Object.assign(body,{ _custom: true, _base64: true })) + const json = JSON.stringify(Object.assign(body, { _custom: true, _base64: true })) return gzipSync(json).toString('base64') } }) class CustomError extends Error { - constructor(message,code) { + constructor(message, code) { super(message) this.name = this.constructor.name this.code = code @@ -51,20 +51,20 @@ let event = { /*** DEFINE TEST MIDDLEWARE & ERRORS ***/ /******************************************************************************/ -api.use(function(req,res,next) { +api.use(function (req, res, next) { req.testMiddleware = '123' next() }); -api.use(function(err,req,res,next) { +api.use(function (err, req, res, next) { req.testError1 = '123' next() }); -api.use(function(err,req,res,next) { +api.use(function (err, req, res, next) { req.testError2 = '456' if (req.path === '/testErrorMiddleware') { - res.header('Content-Type','text/plain') + res.header('Content-Type', 'text/plain') res.send('This is a test error message: ' + req.testError1 + '/' + req.testError2) } else { next() @@ -72,47 +72,47 @@ api.use(function(err,req,res,next) { }); // Add error with promise/delay -api.use(async function(err,req,res,next) { +api.use(async function (err, req, res, next) { if (req.route === '/testErrorPromise') { await delay(100); - res.header('Content-Type','text/plain') + res.header('Content-Type', 'text/plain') res.send('This is a test error message: ' + req.testError1 + '/' + req.testError2) } else { next() } }); -const errorMiddleware1 = (err,req,res,next) => { +const errorMiddleware1 = (err, req, res, next) => { req.errorMiddleware1 = true next() } -const errorMiddleware2 = (err,req,res,next) => { +const errorMiddleware2 = (err, req, res, next) => { req.errorMiddleware2 = true next() } -const sendError = (err,req,res,next) => { +const sendError = (err, req, res, next) => { res.type('text/plain').send('This is a test error message: ' + req.errorMiddleware1 + '/' + req.errorMiddleware2) } -api2.use(errorMiddleware1,errorMiddleware2,sendError) +api2.use(errorMiddleware1, errorMiddleware2, sendError) -const returnError = (err,req,res,next) => { +const returnError = (err, req, res, next) => { return 'this is an error: ' + (req.errorMiddleware1 ? true : false) } -api3.use(returnError,errorMiddleware1) +api3.use(returnError, errorMiddleware1) -const callError = (err,req,res,next) => { +const callError = (err, req, res, next) => { res.status(500).send('this is an error: ' + (req.errorMiddleware1 ? true : false)) next() } -api4.use(callError,errorMiddleware1) +api4.use(callError, errorMiddleware1) -api5.use((err,req,res,next) => { +api5.use((err, req, res, next) => { if (err instanceof CustomError) { res.status(401) } @@ -123,85 +123,85 @@ api5.use((err,req,res,next) => { /*** DEFINE TEST ROUTES ***/ /******************************************************************************/ -api.get('/testError', function(req,res) { +api.get('/testError', function (req, res) { res.error('This is a test error message') }) -api.get('/testErrorThrow', function(req,res) { +api.get('/testErrorThrow', function (req, res) { throw new Error('This is a test thrown error') }) -api.get('/testErrorSimulated', function(req,res) { +api.get('/testErrorSimulated', function (req, res) { res.status(405) res.json({ error: 'This is a simulated error' }) }) -api.get('/testErrorMiddleware', function(req,res) { +api.get('/testErrorMiddleware', function (req, res) { res.error('This test error message should be overridden') }) -api.get('/testErrorPromise', function(req,res) { +api.get('/testErrorPromise', function (req, res) { res.error('This is a test error message') }) -api2.get('/testError', function(req,res) { +api2.get('/testError', function (req, res) { res.status(500) res.error('This is a test error message') }) -api3.get('/testError', function(req,res) { +api3.get('/testError', function (req, res) { res.error('This is a test error message') }) -api4.get('/testError', function(req,res) { - res.error(403,'This is a test error message') +api4.get('/testError', function (req, res) { + res.error(403, 'This is a test error message') }) -api5.get('/testError', function(req,res) { +api5.get('/testError', function (req, res) { res.error('This is a test error message') }) -api5.get('/testErrorThrow', function(req,res) { +api5.get('/testErrorThrow', function (req, res) { throw new Error('This is a test thrown error') }) -api5.get('/testErrorDetail', function(req,res) { - res.error('This is a test error message','details') +api5.get('/testErrorDetail', function (req, res) { + res.error('This is a test error message', 'details') }) -api5.get('/testErrorCustom', function(req,res) { - throw new CustomError('This is a custom error',403) +api5.get('/testErrorCustom', function (req, res) { + throw new CustomError('This is a custom error', 403) }) -api_errors.use(function(err,req,res,next) { +api_errors.use(function (err, req, res, next) { res.send({ errorType: err.name }) }); -api_errors.get('/fileError', (req,res) => { +api_errors.get('/fileError', (req, res) => { res.sendFile('s3://test') }) -api_errors.get('/fileErrorLocal', (req,res) => { +api_errors.get('/fileErrorLocal', (req, res) => { res.sendFile('./missing.txt') }) -api_errors.get('/responseError', (req,res) => { - res.redirect(310,'http://www.google.com') +api_errors.get('/responseError', (req, res) => { + res.redirect(310, 'http://www.google.com') }) -api6.get('/testError', function(req,res) { +api6.get('/testError', function (req, res) { res.error('This is a test error message') }) -api7.get('/testErrorThrow', function(req,res) { +api7.get('/testErrorThrow', function (req, res) { throw new Error('This is a test thrown error') }) -api8.get('/testErrorThrow', function(req,res) { +api8.get('/testErrorThrow', function (req, res) { throw new Error('This is a test thrown error') }) -api9.get('/testErrorThrow', function(req,res) { +api9.get('/testErrorThrow', function (req, res) { throw new Error('This is a test thrown error') }) @@ -209,134 +209,180 @@ api9.get('/testErrorThrow', function(req,res) { /*** BEGIN TESTS ***/ /******************************************************************************/ -describe('Error Handling Tests:', function() { +describe('Error Handling Tests:', function () { // this.slow(300); - describe('Standard', function() { + describe('Standard', function () { - it('Called Error', async function() { - let _event = Object.assign({},event,{ path: '/testError'}) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + it('Called Error', async function () { + let _event = Object.assign({}, event, { path: '/testError' }) + let result = await new Promise(r => api.run(_event, {}, (e, res) => { r(res) })) expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 500, body: '{"error":"This is a test error message"}', isBase64Encoded: false }) }) // end it - it('Thrown Error', async function() { - let _event = Object.assign({},event,{ path: '/testErrorThrow'}) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + it('Thrown Error', async function () { + let _event = Object.assign({}, event, { path: '/testErrorThrow' }) + let result = await new Promise(r => api.run(_event, {}, (e, res) => { r(res) })) expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 500, body: '{"error":"This is a test thrown error"}', isBase64Encoded: false }) }) // end it - it('Simulated Error', async function() { - let _event = Object.assign({},event,{ path: '/testErrorSimulated'}) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + it('Simulated Error', async function () { + let _event = Object.assign({}, event, { path: '/testErrorSimulated' }) + let result = await new Promise(r => api.run(_event, {}, (e, res) => { r(res) })) expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 405, body: '{"error":"This is a simulated error"}', isBase64Encoded: false }) }) // end it }) - describe('Middleware', function() { + describe('Middleware', function () { - it('Error Middleware', async function() { - let _event = Object.assign({},event,{ path: '/testErrorMiddleware'}) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + it('Error Middleware', async function () { + let _event = Object.assign({}, event, { path: '/testErrorMiddleware' }) + let result = await new Promise(r => api.run(_event, {}, (e, res) => { r(res) })) expect(result).toEqual({ multiValueHeaders: { 'content-type': ['text/plain'] }, statusCode: 500, body: 'This is a test error message: 123/456', isBase64Encoded: false }) }) // end it - it('Error Middleware w/ Promise', async function() { - let _event = Object.assign({},event,{ path: '/testErrorPromise'}) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + it('Error Middleware w/ Promise', async function () { + let _event = Object.assign({}, event, { path: '/testErrorPromise' }) + let result = await new Promise(r => api.run(_event, {}, (e, res) => { r(res) })) expect(result).toEqual({ multiValueHeaders: { 'content-type': ['text/plain'] }, statusCode: 500, body: 'This is a test error message: 123/456', isBase64Encoded: false }) }) // end it - it('Multiple error middlewares', async function() { - let _event = Object.assign({},event,{ path: '/testError'}) - let result = await new Promise(r => api2.run(_event,{},(e,res) => { r(res) })) + it('Multiple error middlewares', async function () { + let _event = Object.assign({}, event, { path: '/testError' }) + let result = await new Promise(r => api2.run(_event, {}, (e, res) => { r(res) })) expect(result).toEqual({ multiValueHeaders: { 'content-type': ['text/plain'] }, statusCode: 500, body: 'This is a test error message: true/true', isBase64Encoded: false }) }) // end it - it('Returned error from middleware (async)', async function() { - let _event = Object.assign({},event,{ path: '/testError'}) - let result = await new Promise(r => api3.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { }, statusCode: 500, body: 'this is an error: false', isBase64Encoded: false }) + it('Returned error from middleware (async)', async function () { + let _event = Object.assign({}, event, { path: '/testError' }) + let result = await new Promise(r => api3.run(_event, {}, (e, res) => { r(res) })) + expect(result).toEqual({ multiValueHeaders: {}, statusCode: 500, body: 'this is an error: false', isBase64Encoded: false }) }) // end it - it('Returned error from middleware (callback)', async function() { - let _event = Object.assign({},event,{ path: '/testError'}) - let result = await new Promise(r => api4.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { }, statusCode: 500, body: 'this is an error: false', isBase64Encoded: false }) + it('Returned error from middleware (callback)', async function () { + let _event = Object.assign({}, event, { path: '/testError' }) + let result = await new Promise(r => api4.run(_event, {}, (e, res) => { r(res) })) + expect(result).toEqual({ multiValueHeaders: {}, statusCode: 500, body: 'this is an error: false', isBase64Encoded: false }) }) // end it }) - describe('Error Types', function() { - it('RouteError', async function() { - let _event = Object.assign({},event,{ path: '/testx'}) - let result = await new Promise(r => api_errors.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { }, statusCode: 404, body: '{"errorType":"RouteError"}', isBase64Encoded: false }) + describe('Error Types', function () { + it('RouteError', async function () { + let _event = Object.assign({}, event, { path: '/testx' }) + let result = await new Promise(r => api_errors.run(_event, {}, (e, res) => { r(res) })) + expect(result).toEqual({ multiValueHeaders: {}, statusCode: 404, body: '{"errorType":"RouteError"}', isBase64Encoded: false }) }) // end it - it('RouteError.name', async function() { + it('RouteError.name', async function () { let Error$1 = errors.RouteError let error = new Error$1('This is a test error') expect(error.name).toEqual('RouteError') }) // end it - it('MethodError', async function() { - let _event = Object.assign({},event,{ path: '/fileError', httpMethod: 'put' }) - let result = await new Promise(r => api_errors.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { }, statusCode: 405, body: '{"errorType":"MethodError"}', isBase64Encoded: false }) + it('MethodError', async function () { + let _event = Object.assign({}, event, { path: '/fileError', httpMethod: 'put' }) + let result = await new Promise(r => api_errors.run(_event, {}, (e, res) => { r(res) })) + expect(result).toEqual({ multiValueHeaders: {}, statusCode: 405, body: '{"errorType":"MethodError"}', isBase64Encoded: false }) }) // end it - it('MethodError.name', async function() { + it('MethodError.name', async function () { let Error$1 = errors.MethodError let error = new Error$1('This is a test error') expect(error.name).toEqual('MethodError') }) // end it - it('FileError (s3)', async function() { - let _event = Object.assign({},event,{ path: '/fileError' }) - let result = await new Promise(r => api_errors.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { }, statusCode: 500, body: '{"errorType":"FileError"}', isBase64Encoded: false }) + it('FileError (s3)', async function () { + let _event = Object.assign({}, event, { path: '/fileError' }) + let result = await new Promise(r => api_errors.run(_event, {}, (e, res) => { r(res) })) + expect(result).toEqual({ multiValueHeaders: {}, statusCode: 500, body: '{"errorType":"FileError"}', isBase64Encoded: false }) }) // end it - it('FileError (local)', async function() { - let _event = Object.assign({},event,{ path: '/fileErrorLocal' }) - let result = await new Promise(r => api_errors.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { }, statusCode: 500, body: '{"errorType":"FileError"}', isBase64Encoded: false }) + it('FileError (local)', async function () { + let _event = Object.assign({}, event, { path: '/fileErrorLocal' }) + let result = await new Promise(r => api_errors.run(_event, {}, (e, res) => { r(res) })) + expect(result).toEqual({ multiValueHeaders: {}, statusCode: 500, body: '{"errorType":"FileError"}', isBase64Encoded: false }) }) // end it - it('FileError.name', async function() { + it('FileError.name', async function () { let Error$1 = errors.FileError let error = new Error$1('This is a test error') expect(error.name).toEqual('FileError') }) // end it - it('ResponseError', async function() { - let _event = Object.assign({},event,{ path: '/responseError' }) - let result = await new Promise(r => api_errors.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { }, statusCode: 500, body: '{"errorType":"ResponseError"}', isBase64Encoded: false }) + it('ResponseError', async function () { + let _event = Object.assign({}, event, { path: '/responseError' }) + let result = await new Promise(r => api_errors.run(_event, {}, (e, res) => { r(res) })) + expect(result).toEqual({ multiValueHeaders: {}, statusCode: 500, body: '{"errorType":"ResponseError"}', isBase64Encoded: false }) }) // end it - it('ResponseError.name', async function() { + it('ResponseError.name', async function () { let Error$1 = errors.ResponseError let error = new Error$1('This is a test error') expect(error.name).toEqual('ResponseError') }) // end it - it('ConfigurationError.name', async function() { + it('ConfigurationError.name', async function () { let Error$1 = errors.ConfigurationError let error = new Error$1('This is a test error') expect(error.name).toEqual('ConfigurationError') }) // end it + + it('ApiError with string message', async function () { + let _event = Object.assign({}, event, { path: '/testError' }); + let result = await new Promise(r => api5.run(_event, {}, (e, res) => { r(res) })); + expect(result).toEqual({ + multiValueHeaders: { 'content-type': ['application/json'] }, + statusCode: 500, + body: '{"error":"This is a test error message"}', + isBase64Encoded: false + }); + }); + + it('ApiError with code and message', async function () { + let _event = Object.assign({}, event, { path: '/testError' }); + let result = await new Promise(r => api4.run(_event, {}, (e, res) => { r(res) })); + expect(result).toEqual({ + multiValueHeaders: {}, + statusCode: 500, + body: 'this is an error: false', + isBase64Encoded: false + }); + }); + + it('ApiError with message and detail', async function () { + let _event = Object.assign({}, event, { path: '/testErrorDetail' }); + let result = await new Promise(r => api5.run(_event, {}, (e, res) => { r(res) })); + expect(result).toEqual({ + multiValueHeaders: { 'content-type': ['application/json'] }, + statusCode: 500, + body: '{"error":"This is a test error message"}', + isBase64Encoded: false + }); + }); + + it('ApiError properties', function () { + const error = new errors.ApiError('test message', 403, { foo: 'bar' }); + expect(error.name).toBe('ApiError'); + expect(error.message).toBe('test message'); + expect(error.code).toBe(403); + expect(error.detail).toEqual({ foo: 'bar' }); + }); + + it('ApiError default code', function () { + const error = new errors.ApiError('test message'); + expect(error.code).toBe(500); + }); }) - describe('Logging', function() { + describe('Logging', function () { - it('Thrown Error', async function() { + it('Thrown Error', async function () { let _log - let _event = Object.assign({},event,{ path: '/testErrorThrow'}) + let _event = Object.assign({}, event, { path: '/testErrorThrow' }) let logger = console.log - console.log = log => { try { _log = JSON.parse(log) } catch(e) { _log = log } } - let result = await new Promise(r => api5.run(_event,{},(e,res) => { r(res) })) + console.log = log => { try { _log = JSON.parse(log) } catch (e) { _log = log } } + let result = await new Promise(r => api5.run(_event, {}, (e, res) => { r(res) })) console.log = logger expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 500, body: '{"error":"This is a test thrown error"}', isBase64Encoded: false }) expect(_log.level).toBe('fatal') @@ -344,24 +390,24 @@ describe('Error Handling Tests:', function() { }) // end it - it('API Error', async function() { + it('API Error', async function () { let _log - let _event = Object.assign({},event,{ path: '/testError'}) + let _event = Object.assign({}, event, { path: '/testError' }) let logger = console.log - console.log = log => { try { _log = JSON.parse(log) } catch(e) { _log = log } } - let result = await new Promise(r => api5.run(_event,{},(e,res) => { r(res) })) + console.log = log => { try { _log = JSON.parse(log) } catch (e) { _log = log } } + let result = await new Promise(r => api5.run(_event, {}, (e, res) => { r(res) })) console.log = logger expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 500, body: '{"error":"This is a test error message"}', isBase64Encoded: false }) expect(_log.level).toBe('error') expect(_log.msg).toBe('This is a test error message') }) // end it - it('Error with Detail', async function() { + it('Error with Detail', async function () { let _log - let _event = Object.assign({},event,{ path: '/testErrorDetail'}) + let _event = Object.assign({}, event, { path: '/testErrorDetail' }) let logger = console.log - console.log = log => { try { _log = JSON.parse(log) } catch(e) { _log = log } } - let result = await new Promise(r => api5.run(_event,{},(e,res) => { r(res) })) + console.log = log => { try { _log = JSON.parse(log) } catch (e) { _log = log } } + let result = await new Promise(r => api5.run(_event, {}, (e, res) => { r(res) })) console.log = logger expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 500, body: '{"error":"This is a test error message"}', isBase64Encoded: false }) expect(_log.level).toBe('error') @@ -369,12 +415,12 @@ describe('Error Handling Tests:', function() { expect(_log.detail).toBe('details') }) // end it - it('Custom Error', async function() { + it('Custom Error', async function () { let _log - let _event = Object.assign({},event,{ path: '/testErrorCustom'}) + let _event = Object.assign({}, event, { path: '/testErrorCustom' }) let logger = console.log - console.log = log => { try { _log = JSON.parse(log) } catch(e) { _log = log } } - let result = await new Promise(r => api5.run(_event,{},(e,res) => { r(res) })) + console.log = log => { try { _log = JSON.parse(log) } catch (e) { _log = log } } + let result = await new Promise(r => api5.run(_event, {}, (e, res) => { r(res) })) console.log = logger // console.log(JSON.stringify(_log,null,2)); expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 401, body: '{"error":"This is a custom error"}', isBase64Encoded: false }) @@ -383,33 +429,33 @@ describe('Error Handling Tests:', function() { }) // end it - it('Error, no props', async function() { + it('Error, no props', async function () { let _log - let _event = Object.assign({},event,{ path: '/testError'}) + let _event = Object.assign({}, event, { path: '/testError' }) let logger = console.log - console.log = log => { try { _log = JSON.parse(log) } catch(e) { _log = log } } - let result = await new Promise(r => api6.run(_event,{},(e,res) => { r(res) })) + console.log = log => { try { _log = JSON.parse(log) } catch (e) { _log = log } } + let result = await new Promise(r => api6.run(_event, {}, (e, res) => { r(res) })) console.log = logger expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 500, body: '{"error":"This is a test error message"}', isBase64Encoded: false }) }) // end it - it('Should not log error if option logger.errorLogging is false', async function() { + it('Should not log error if option logger.errorLogging is false', async function () { let _log - let _event = Object.assign({},event,{ path: '/testErrorThrow'}) + let _event = Object.assign({}, event, { path: '/testErrorThrow' }) let logger = console.log - console.log = log => { try { _log = JSON.parse(log) } catch(e) { _log = log } } - let result = await new Promise(r => api7.run(_event,{},(e,res) => { r(res) })) + console.log = log => { try { _log = JSON.parse(log) } catch (e) { _log = log } } + let result = await new Promise(r => api7.run(_event, {}, (e, res) => { r(res) })) console.log = logger expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 500, body: '{"error":"This is a test thrown error"}', isBase64Encoded: false }) expect(_log).toBe(undefined) }) - it('Should log error if option logger.errorLogging is true', async function() { + it('Should log error if option logger.errorLogging is true', async function () { let _log - let _event = Object.assign({},event,{ path: '/testErrorThrow'}) + let _event = Object.assign({}, event, { path: '/testErrorThrow' }) let logger = console.log - console.log = log => { try { _log = JSON.parse(log) } catch(e) { _log = log } } - let result = await new Promise(r => api8.run(_event,{},(e,res) => { r(res) })) + console.log = log => { try { _log = JSON.parse(log) } catch (e) { _log = log } } + let result = await new Promise(r => api8.run(_event, {}, (e, res) => { r(res) })) console.log = logger expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 500, body: '{"error":"This is a test thrown error"}', isBase64Encoded: false }) expect(_log.level).toBe('fatal') @@ -418,13 +464,13 @@ describe('Error Handling Tests:', function() { }) - describe('base64 errors', function() { - it('Should return errors with base64 encoding', async function() { + describe('base64 errors', function () { + it('Should return errors with base64 encoding', async function () { let _log - let _event = Object.assign({},event,{ path: '/testErrorThrow'}) + let _event = Object.assign({}, event, { path: '/testErrorThrow' }) let logger = console.log - console.log = log => { try { _log = JSON.parse(log) } catch(e) { _log = log } } - let result = await new Promise(r => api9.run(_event,{},(e,res) => { r(res) })) + console.log = log => { try { _log = JSON.parse(log) } catch (e) { _log = log } } + let result = await new Promise(r => api9.run(_event, {}, (e, res) => { r(res) })) console.log = logger let body = gzipSync(`{"error":"This is a test thrown error","_custom":true,"_base64":true}`).toString('base64') expect(result).toEqual({ multiValueHeaders: { 'content-encoding': ['gzip'], 'content-type': ['application/json'] }, statusCode: 500, body, isBase64Encoded: true }) diff --git a/index.d.ts b/index.d.ts index c23ae10..d87a09e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -369,6 +369,13 @@ export declare class ResponseError extends Error { constructor(message: string, code: number); } +export declare class ApiError extends Error { + constructor(message: string, code?: number, detail?: any); + name: 'ApiError'; + code?: number; + detail?: any; +} + export declare class FileError extends Error { constructor(message: string, err: object); } diff --git a/index.js b/index.js index 86e3439..58a544d 100644 --- a/index.js +++ b/index.js @@ -10,8 +10,8 @@ const RESPONSE = require('./lib/response'); const UTILS = require('./lib/utils'); const LOGGER = require('./lib/logger'); const S3 = () => require('./lib/s3-service'); +const { ConfigurationError, ApiError } = require('./lib/errors'); const prettyPrint = require('./lib/prettyPrint'); -const { ConfigurationError } = require('./lib/errors'); class API { constructor(props) { @@ -328,26 +328,21 @@ class API { // Catch all async/sync errors async catchErrors(e, response, code, detail) { - // Error messages should respect the app's base64 configuration response._isBase64 = this._isBase64; - // Strip the headers, keep whitelist const strippedHeaders = Object.entries(response._headers).reduce( (acc, [headerName, value]) => { if (!this._errorHeaderWhitelist.includes(headerName.toLowerCase())) { return acc; } - return Object.assign(acc, { [headerName]: value }); }, {} ); response._headers = Object.assign(strippedHeaders, this._headers); - let message; - // Set the status code response.status(code ? code : this._errorStatus); let info = { @@ -357,13 +352,15 @@ class API { stack: (this._logger.stack && e.stack) || undefined, }; - if (e instanceof Error) { + const isApiError = e instanceof ApiError; + + if (e instanceof Error && !isApiError) { message = e.message; if (this._logger.errorLogging) { this.log.fatal(message, info); } } else { - message = e; + message = e instanceof Error ? e.message : e; if (this._logger.errorLogging) { this.log.error(message, info); } @@ -387,7 +384,7 @@ class API { if (rtn) response.send(rtn); r(); }); - } // end for + } } // Throw standard error unless callback has already been executed diff --git a/index.test-d.ts b/index.test-d.ts index 782501b..e4b68bd 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -17,6 +17,7 @@ import { ConfigurationError, ResponseError, FileError, + ApiError, } from './index'; import { APIGatewayProxyEvent, @@ -255,3 +256,12 @@ const fileError = new FileError('File not found', { syscall: 'open', }); expectType(fileError); +expectType(fileError.message); +expectType(fileError.name); +expectType(fileError.stack); + +const apiError = new ApiError('Api error', 500); +expectType(apiError); +expectType(apiError.message); +expectType(apiError.code); +expectType(apiError.detail); diff --git a/lib/errors.js b/lib/errors.js index 8a0df63..e4baaa7 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -41,6 +41,17 @@ class ResponseError extends Error { } } +class ApiError extends Error { + constructor(message, code, detail) { + super(message); + this.name = 'ApiError'; + this.code = typeof code === 'number' ? code : 500; + if (detail !== undefined) { + this.detail = detail; + } + } +} + class FileError extends Error { constructor(message, err) { super(message); @@ -55,5 +66,6 @@ module.exports = { MethodError, ConfigurationError, ResponseError, + ApiError, FileError, }; diff --git a/lib/response.js b/lib/response.js index cf4d0d2..3986d88 100644 --- a/lib/response.js +++ b/lib/response.js @@ -10,7 +10,7 @@ const UTILS = require('./utils.js'); const fs = require('fs'); // Require Node.js file system const path = require('path'); // Require Node.js path const compression = require('./compression'); // Require compression lib -const { ResponseError, FileError } = require('./errors'); // Require custom errors +const { ResponseError, FileError, ApiError } = require('./errors'); // Require custom errors // Lazy load AWS S3 service const S3 = () => require('./s3-service'); @@ -248,7 +248,7 @@ class RESPONSE { // secure (Boolean): Marks the cookie to be used with HTTPS only cookieString += opts.secure && opts.secure === true ? '; Secure' : ''; - // sameSite (Boolean or String) Value of the “SameSite” Set-Cookie attribute + // sameSite (Boolean or String) Value of the "SameSite" Set-Cookie attribute // see https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1. cookieString += opts.sameSite !== undefined @@ -594,10 +594,17 @@ class RESPONSE { // Trigger API error error(code, e, detail) { - detail = typeof code !== 'number' && e !== undefined ? e : detail; - e = typeof code !== 'number' ? code : e; - code = typeof code === 'number' ? code : undefined; - this.app.catchErrors(e, this, code, detail); + const message = typeof code !== 'number' ? code : e; + const statusCode = typeof code === 'number' ? code : undefined; + const errorDetail = + typeof code !== 'number' && e !== undefined ? e : detail; + + const errorToSend = + typeof message === 'string' + ? new ApiError(message, statusCode, errorDetail) + : message; + + this.app.catchErrors(errorToSend, this, statusCode, errorDetail); } // end error } // end Response class