From 8c567373db9cb6a50d129d382c36a2d1db7fb659 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Fri, 4 Apr 2025 23:01:13 +0300 Subject: [PATCH 01/17] fix(error-handling): treat res.error(message string) as regular error --- __tests__/errorHandling.unit.js | 312 ++++++++++++++++++-------------- index.d.ts | 5 +- index.js | 27 +-- lib/errors.js | 9 +- lib/response.js | 71 ++++---- 5 files changed, 244 insertions(+), 180 deletions(-) diff --git a/__tests__/errorHandling.unit.js b/__tests__/errorHandling.unit.js index ee14074..1d5005f 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('ResponseError 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('ResponseError 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('ResponseError 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('ResponseError properties', function () { + const error = new errors.ResponseError('test message', 403, { foo: 'bar' }); + expect(error.name).toBe('ResponseError'); + expect(error.message).toBe('test message'); + expect(error.code).toBe(403); + expect(error.detail).toEqual({ foo: 'bar' }); + }); + + it('ResponseError default code', function () { + const error = new errors.ResponseError('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..23324ea 100644 --- a/index.d.ts +++ b/index.d.ts @@ -366,7 +366,10 @@ export declare class ConfigurationError extends Error { } export declare class ResponseError extends Error { - constructor(message: string, code: number); + constructor(message: string, code?: number, detail?: any); + name: 'ResponseError'; + code: number; + detail?: any; } export declare class FileError extends Error { diff --git a/index.js b/index.js index 86e3439..4079126 100644 --- a/index.js +++ b/index.js @@ -9,9 +9,11 @@ const REQUEST = require('./lib/request'); const RESPONSE = require('./lib/response'); const UTILS = require('./lib/utils'); const LOGGER = require('./lib/logger'); -const S3 = () => require('./lib/s3-service'); +const { ResponseError, ConfigurationError } = require('./lib/errors'); const prettyPrint = require('./lib/prettyPrint'); -const { ConfigurationError } = require('./lib/errors'); + +// Lazy load AWS S3 service +const S3 = () => require('./lib/s3-service'); class API { constructor(props) { @@ -42,8 +44,8 @@ class API { : {}; this._compression = props && - (typeof props.compression === 'boolean' || - Array.isArray(props.compression)) + (typeof props.compression === 'boolean' || + Array.isArray(props.compression)) ? props.compression : false; @@ -84,7 +86,7 @@ class API { this._app = {}; // Executed after the callback - this._finally = () => {}; + this._finally = () => { }; // Global error status (used for response parsing errors) this._errorStatus = 500; @@ -213,8 +215,8 @@ class API { stack: _stack['m'][method] ? _stack['m'][method].concat(stack) : _stack['*'][method] - ? _stack['*'][method].concat(stack) - : stack, + ? _stack['*'][method].concat(stack) + : stack, // inherited: _stack[method] ? _stack[method] : [], route: '/' + parsedPath.join('/'), path: '/' + this._prefix.concat(parsedPath).join('/'), @@ -357,13 +359,16 @@ class API { stack: (this._logger.stack && e.stack) || undefined, }; - if (e instanceof Error) { + // Determine if this was originally a string error + const wasStringError = e instanceof ResponseError && e.originalMessage !== undefined; + + if (e instanceof Error && !wasStringError) { 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); } @@ -450,8 +455,8 @@ class API { typeof args[0] === 'string' ? Array.of(args.shift()) : Array.isArray(args[0]) - ? args.shift() - : ['/*']; + ? args.shift() + : ['/*']; // Init middleware stack let middleware = []; diff --git a/lib/errors.js b/lib/errors.js index 8a0df63..9e1c8a8 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -34,10 +34,15 @@ class ConfigurationError extends Error { } class ResponseError extends Error { - constructor(message, code) { + constructor(message, code, detail) { super(message); this.name = 'ResponseError'; - this.code = code; + this.code = typeof code === 'number' ? code : 500; + if (detail !== undefined) { + this.detail = detail; + } + // Track if this error was created from a string message + this.originalMessage = typeof message === 'string' ? message : undefined; } } diff --git a/lib/response.js b/lib/response.js index cf4d0d2..0b9c541 100644 --- a/lib/response.js +++ b/lib/response.js @@ -85,15 +85,15 @@ class RESPONSE { return asArr ? this._headers : Object.keys(this._headers).reduce( - (headers, key) => - Object.assign(headers, { [key]: this._headers[key].toString() }), - {} - ); // return all headers + (headers, key) => + Object.assign(headers, { [key]: this._headers[key].toString() }), + {} + ); // return all headers return asArr ? this._headers[key.toLowerCase()] : this._headers[key.toLowerCase()] - ? this._headers[key.toLowerCase()].toString() - : undefined; + ? this._headers[key.toLowerCase()].toString() + : undefined; } // Issue #130 @@ -131,9 +131,9 @@ class RESPONSE { this.header('Content-Type', 'application/json').send( (cb ? cb.replace(' ', '_') : 'callback') + - '(' + - this._serializer(body) + - ')' + '(' + + this._serializer(body) + + ')' ); } @@ -193,8 +193,8 @@ class RESPONSE { typeof expires === 'function' ? expires : typeof callback === 'function' - ? callback - : (e) => { + ? callback + : (e) => { if (e) this.error(e); }; @@ -236,10 +236,10 @@ class RESPONSE { cookieString += opts.maxAge && !isNaN(opts.maxAge) ? '; MaxAge=' + - ((opts.maxAge / 1000) | 0) + - (!opts.expires - ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() - : '') + ((opts.maxAge / 1000) | 0) + + (!opts.expires + ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() + : '') : ''; // path (String): Path for the cookie @@ -253,9 +253,9 @@ class RESPONSE { cookieString += opts.sameSite !== undefined ? '; SameSite=' + - (opts.sameSite === true - ? 'Strict' - : opts.sameSite === false + (opts.sameSite === true + ? 'Strict' + : opts.sameSite === false ? 'Lax' : opts.sameSite) : ''; @@ -323,7 +323,7 @@ class RESPONSE { let buffer, modified; let opts = typeof options === 'object' ? options : {}; - let fn = typeof callback === 'function' ? callback : () => {}; + let fn = typeof callback === 'function' ? callback : () => { }; // Add optional parameter support if (typeof options === 'function') { @@ -440,16 +440,16 @@ class RESPONSE { opts.methods ? opts.methods : acam - ? acam - : 'GET, PUT, POST, DELETE, OPTIONS' + ? acam + : 'GET, PUT, POST, DELETE, OPTIONS' ); this.header( 'Access-Control-Allow-Headers', opts.headers ? opts.headers : acah - ? acah - : 'Content-Type, Authorization, Content-Length, X-Requested-With' + ? acah + : 'Content-Type, Authorization, Content-Length, X-Requested-With' ); // Optional CORS headers @@ -500,8 +500,8 @@ class RESPONSE { date && typeof date.toUTCString === 'function' ? date : date && Date.parse(date) - ? new Date(date) - : new Date(); + ? new Date(date) + : new Date(); this.header('Last-Modified', lastModified.toUTCString()); } return this; @@ -559,10 +559,10 @@ class RESPONSE { }, this._request.interface === 'alb' ? { - statusDescription: `${this._statusCode} ${UTILS.statusLookup( - this._statusCode - )}`, - } + statusDescription: `${this._statusCode} ${UTILS.statusLookup( + this._statusCode + )}`, + } : {} ); @@ -594,10 +594,15 @@ 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 ResponseError(message, statusCode, errorDetail) + : message; + + this.app.catchErrors(errorToSend, this, statusCode, errorDetail); } // end error } // end Response class From 87d4ff67c5c8d4605b64b1e97f56be5c18ba804f Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Fri, 4 Apr 2025 23:02:38 +0300 Subject: [PATCH 02/17] prettier --- index.js | 17 +++++++------ lib/response.js | 68 +++++++++++++++++++++++++------------------------ 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/index.js b/index.js index 4079126..cf26ad0 100644 --- a/index.js +++ b/index.js @@ -44,8 +44,8 @@ class API { : {}; this._compression = props && - (typeof props.compression === 'boolean' || - Array.isArray(props.compression)) + (typeof props.compression === 'boolean' || + Array.isArray(props.compression)) ? props.compression : false; @@ -86,7 +86,7 @@ class API { this._app = {}; // Executed after the callback - this._finally = () => { }; + this._finally = () => {}; // Global error status (used for response parsing errors) this._errorStatus = 500; @@ -215,8 +215,8 @@ class API { stack: _stack['m'][method] ? _stack['m'][method].concat(stack) : _stack['*'][method] - ? _stack['*'][method].concat(stack) - : stack, + ? _stack['*'][method].concat(stack) + : stack, // inherited: _stack[method] ? _stack[method] : [], route: '/' + parsedPath.join('/'), path: '/' + this._prefix.concat(parsedPath).join('/'), @@ -360,7 +360,8 @@ class API { }; // Determine if this was originally a string error - const wasStringError = e instanceof ResponseError && e.originalMessage !== undefined; + const wasStringError = + e instanceof ResponseError && e.originalMessage !== undefined; if (e instanceof Error && !wasStringError) { message = e.message; @@ -455,8 +456,8 @@ class API { typeof args[0] === 'string' ? Array.of(args.shift()) : Array.isArray(args[0]) - ? args.shift() - : ['/*']; + ? args.shift() + : ['/*']; // Init middleware stack let middleware = []; diff --git a/lib/response.js b/lib/response.js index 0b9c541..f9319be 100644 --- a/lib/response.js +++ b/lib/response.js @@ -85,15 +85,15 @@ class RESPONSE { return asArr ? this._headers : Object.keys(this._headers).reduce( - (headers, key) => - Object.assign(headers, { [key]: this._headers[key].toString() }), - {} - ); // return all headers + (headers, key) => + Object.assign(headers, { [key]: this._headers[key].toString() }), + {} + ); // return all headers return asArr ? this._headers[key.toLowerCase()] : this._headers[key.toLowerCase()] - ? this._headers[key.toLowerCase()].toString() - : undefined; + ? this._headers[key.toLowerCase()].toString() + : undefined; } // Issue #130 @@ -131,9 +131,9 @@ class RESPONSE { this.header('Content-Type', 'application/json').send( (cb ? cb.replace(' ', '_') : 'callback') + - '(' + - this._serializer(body) + - ')' + '(' + + this._serializer(body) + + ')' ); } @@ -193,8 +193,8 @@ class RESPONSE { typeof expires === 'function' ? expires : typeof callback === 'function' - ? callback - : (e) => { + ? callback + : (e) => { if (e) this.error(e); }; @@ -236,10 +236,10 @@ class RESPONSE { cookieString += opts.maxAge && !isNaN(opts.maxAge) ? '; MaxAge=' + - ((opts.maxAge / 1000) | 0) + - (!opts.expires - ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() - : '') + ((opts.maxAge / 1000) | 0) + + (!opts.expires + ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() + : '') : ''; // path (String): Path for the cookie @@ -253,9 +253,9 @@ class RESPONSE { cookieString += opts.sameSite !== undefined ? '; SameSite=' + - (opts.sameSite === true - ? 'Strict' - : opts.sameSite === false + (opts.sameSite === true + ? 'Strict' + : opts.sameSite === false ? 'Lax' : opts.sameSite) : ''; @@ -323,7 +323,7 @@ class RESPONSE { let buffer, modified; let opts = typeof options === 'object' ? options : {}; - let fn = typeof callback === 'function' ? callback : () => { }; + let fn = typeof callback === 'function' ? callback : () => {}; // Add optional parameter support if (typeof options === 'function') { @@ -440,16 +440,16 @@ class RESPONSE { opts.methods ? opts.methods : acam - ? acam - : 'GET, PUT, POST, DELETE, OPTIONS' + ? acam + : 'GET, PUT, POST, DELETE, OPTIONS' ); this.header( 'Access-Control-Allow-Headers', opts.headers ? opts.headers : acah - ? acah - : 'Content-Type, Authorization, Content-Length, X-Requested-With' + ? acah + : 'Content-Type, Authorization, Content-Length, X-Requested-With' ); // Optional CORS headers @@ -500,8 +500,8 @@ class RESPONSE { date && typeof date.toUTCString === 'function' ? date : date && Date.parse(date) - ? new Date(date) - : new Date(); + ? new Date(date) + : new Date(); this.header('Last-Modified', lastModified.toUTCString()); } return this; @@ -559,10 +559,10 @@ class RESPONSE { }, this._request.interface === 'alb' ? { - statusDescription: `${this._statusCode} ${UTILS.statusLookup( - this._statusCode - )}`, - } + statusDescription: `${this._statusCode} ${UTILS.statusLookup( + this._statusCode + )}`, + } : {} ); @@ -596,11 +596,13 @@ class RESPONSE { error(code, e, 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 errorDetail = + typeof code !== 'number' && e !== undefined ? e : detail; - const errorToSend = typeof message === 'string' - ? new ResponseError(message, statusCode, errorDetail) - : message; + const errorToSend = + typeof message === 'string' + ? new ResponseError(message, statusCode, errorDetail) + : message; this.app.catchErrors(errorToSend, this, statusCode, errorDetail); } // end error From a488b86f752720099893e59724c1d9ca39e3c2dd Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Fri, 4 Apr 2025 23:03:25 +0300 Subject: [PATCH 03/17] revert unecessary change --- index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 23324ea..f0043f0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -367,7 +367,6 @@ export declare class ConfigurationError extends Error { export declare class ResponseError extends Error { constructor(message: string, code?: number, detail?: any); - name: 'ResponseError'; code: number; detail?: any; } From 1782587ae6315bff38fcb9d785d31d795ecf3e4b Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Fri, 4 Apr 2025 23:04:34 +0300 Subject: [PATCH 04/17] fix type def --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index f0043f0..b210af0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -367,7 +367,7 @@ export declare class ConfigurationError extends Error { export declare class ResponseError extends Error { constructor(message: string, code?: number, detail?: any); - code: number; + code?: number; detail?: any; } From 6c3ce125fbb13130406e0ac4fb894aaa8340671c Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Fri, 4 Apr 2025 23:05:37 +0300 Subject: [PATCH 05/17] . --- index.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index cf26ad0..ae85345 100644 --- a/index.js +++ b/index.js @@ -9,12 +9,10 @@ const REQUEST = require('./lib/request'); const RESPONSE = require('./lib/response'); const UTILS = require('./lib/utils'); const LOGGER = require('./lib/logger'); +const S3 = () => require('./lib/s3-service'); const { ResponseError, ConfigurationError } = require('./lib/errors'); const prettyPrint = require('./lib/prettyPrint'); -// Lazy load AWS S3 service -const S3 = () => require('./lib/s3-service'); - class API { constructor(props) { this._version = props && props.version ? props.version : 'v1'; @@ -44,8 +42,8 @@ class API { : {}; this._compression = props && - (typeof props.compression === 'boolean' || - Array.isArray(props.compression)) + (typeof props.compression === 'boolean' || + Array.isArray(props.compression)) ? props.compression : false; @@ -86,7 +84,7 @@ class API { this._app = {}; // Executed after the callback - this._finally = () => {}; + this._finally = () => { }; // Global error status (used for response parsing errors) this._errorStatus = 500; @@ -215,8 +213,8 @@ class API { stack: _stack['m'][method] ? _stack['m'][method].concat(stack) : _stack['*'][method] - ? _stack['*'][method].concat(stack) - : stack, + ? _stack['*'][method].concat(stack) + : stack, // inherited: _stack[method] ? _stack[method] : [], route: '/' + parsedPath.join('/'), path: '/' + this._prefix.concat(parsedPath).join('/'), @@ -456,8 +454,8 @@ class API { typeof args[0] === 'string' ? Array.of(args.shift()) : Array.isArray(args[0]) - ? args.shift() - : ['/*']; + ? args.shift() + : ['/*']; // Init middleware stack let middleware = []; From 5a5ee4af19a9c5ba1f6b18a39fa542540c9ea090 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Fri, 4 Apr 2025 23:05:55 +0300 Subject: [PATCH 06/17] unecessary comment --- index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/index.js b/index.js index ae85345..e4ff780 100644 --- a/index.js +++ b/index.js @@ -357,7 +357,6 @@ class API { stack: (this._logger.stack && e.stack) || undefined, }; - // Determine if this was originally a string error const wasStringError = e instanceof ResponseError && e.originalMessage !== undefined; From 58a48236402db2e0215a9d9193df021f41876719 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Fri, 4 Apr 2025 23:07:36 +0300 Subject: [PATCH 07/17] . --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index b210af0..f0043f0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -367,7 +367,7 @@ export declare class ConfigurationError extends Error { export declare class ResponseError extends Error { constructor(message: string, code?: number, detail?: any); - code?: number; + code: number; detail?: any; } From 786e9ee18e724c57f53b1efd842118a0696ba3ad Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Fri, 4 Apr 2025 23:10:16 +0300 Subject: [PATCH 08/17] prettier --- index.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index e4ff780..37056ba 100644 --- a/index.js +++ b/index.js @@ -42,8 +42,8 @@ class API { : {}; this._compression = props && - (typeof props.compression === 'boolean' || - Array.isArray(props.compression)) + (typeof props.compression === 'boolean' || + Array.isArray(props.compression)) ? props.compression : false; @@ -84,7 +84,7 @@ class API { this._app = {}; // Executed after the callback - this._finally = () => { }; + this._finally = () => {}; // Global error status (used for response parsing errors) this._errorStatus = 500; @@ -213,8 +213,8 @@ class API { stack: _stack['m'][method] ? _stack['m'][method].concat(stack) : _stack['*'][method] - ? _stack['*'][method].concat(stack) - : stack, + ? _stack['*'][method].concat(stack) + : stack, // inherited: _stack[method] ? _stack[method] : [], route: '/' + parsedPath.join('/'), path: '/' + this._prefix.concat(parsedPath).join('/'), @@ -453,8 +453,8 @@ class API { typeof args[0] === 'string' ? Array.of(args.shift()) : Array.isArray(args[0]) - ? args.shift() - : ['/*']; + ? args.shift() + : ['/*']; // Init middleware stack let middleware = []; From 169f4d48e114e7dbe554a415f920b4474af12b10 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 5 Apr 2025 19:50:06 +0300 Subject: [PATCH 09/17] . --- __tests__/errorHandling.unit.js | 46 +++++++++++++++++++++ index.d.ts | 7 +++- index.js | 36 +++++------------ lib/errors.js | 15 +++++-- lib/response.js | 72 ++++++++++++++++----------------- 5 files changed, 109 insertions(+), 67 deletions(-) diff --git a/__tests__/errorHandling.unit.js b/__tests__/errorHandling.unit.js index 1d5005f..8dec498 100644 --- a/__tests__/errorHandling.unit.js +++ b/__tests__/errorHandling.unit.js @@ -373,6 +373,52 @@ describe('Error Handling Tests:', function () { const error = new errors.ResponseError('test message'); expect(error.code).toBe(500); }); + + 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 () { diff --git a/index.d.ts b/index.d.ts index f0043f0..d87a09e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -366,8 +366,13 @@ export declare class ConfigurationError extends Error { } export declare class ResponseError extends Error { + constructor(message: string, code: number); +} + +export declare class ApiError extends Error { constructor(message: string, code?: number, detail?: any); - code: number; + name: 'ApiError'; + code?: number; detail?: any; } diff --git a/index.js b/index.js index 37056ba..6bee7c6 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,7 @@ const RESPONSE = require('./lib/response'); const UTILS = require('./lib/utils'); const LOGGER = require('./lib/logger'); const S3 = () => require('./lib/s3-service'); -const { ResponseError, ConfigurationError } = require('./lib/errors'); +const { ResponseError, ConfigurationError, ApiError } = require('./lib/errors'); const prettyPrint = require('./lib/prettyPrint'); class API { @@ -42,8 +42,8 @@ class API { : {}; this._compression = props && - (typeof props.compression === 'boolean' || - Array.isArray(props.compression)) + (typeof props.compression === 'boolean' || + Array.isArray(props.compression)) ? props.compression : false; @@ -84,7 +84,7 @@ class API { this._app = {}; // Executed after the callback - this._finally = () => {}; + this._finally = () => { }; // Global error status (used for response parsing errors) this._errorStatus = 500; @@ -213,8 +213,8 @@ class API { stack: _stack['m'][method] ? _stack['m'][method].concat(stack) : _stack['*'][method] - ? _stack['*'][method].concat(stack) - : stack, + ? _stack['*'][method].concat(stack) + : stack, // inherited: _stack[method] ? _stack[method] : [], route: '/' + parsedPath.join('/'), path: '/' + this._prefix.concat(parsedPath).join('/'), @@ -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,10 +352,9 @@ class API { stack: (this._logger.stack && e.stack) || undefined, }; - const wasStringError = - e instanceof ResponseError && e.originalMessage !== undefined; + const isApiError = e instanceof ApiError; - if (e instanceof Error && !wasStringError) { + if (e instanceof Error && !isApiError) { message = e.message; if (this._logger.errorLogging) { this.log.fatal(message, info); @@ -372,17 +366,10 @@ class API { } } - // If first time through, process error middleware if (response._state === 'processing') { - // Flag error state (this will avoid infinite error loops) response._state = 'error'; - - // Execute error middleware for (const err of this._errors) { if (response._state === 'done') break; - // Promisify error middleware - // TODO: using async within a promise is an antipattern, therefore we need to refactor this asap - // eslint-disable-next-line no-async-promise-executor await new Promise(async (r) => { let rtn = await err(e, response._request, response, () => { r(); @@ -390,10 +377,9 @@ class API { if (rtn) response.send(rtn); r(); }); - } // end for + } } - // Throw standard error unless callback has already been executed if (response._state !== 'done') response.json({ error: message }); } // end catch @@ -453,8 +439,8 @@ class API { typeof args[0] === 'string' ? Array.of(args.shift()) : Array.isArray(args[0]) - ? args.shift() - : ['/*']; + ? args.shift() + : ['/*']; // Init middleware stack let middleware = []; diff --git a/lib/errors.js b/lib/errors.js index 9e1c8a8..7d05846 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -34,15 +34,21 @@ class ConfigurationError extends Error { } class ResponseError extends Error { - constructor(message, code, detail) { + constructor(message, code) { super(message); this.name = 'ResponseError'; + this.code = code; + } +} + +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; } - // Track if this error was created from a string message - this.originalMessage = typeof message === 'string' ? message : undefined; } } @@ -60,5 +66,6 @@ module.exports = { MethodError, ConfigurationError, ResponseError, - FileError, + ApiError, + FileError }; diff --git a/lib/response.js b/lib/response.js index f9319be..ea1e987 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'); @@ -85,15 +85,15 @@ class RESPONSE { return asArr ? this._headers : Object.keys(this._headers).reduce( - (headers, key) => - Object.assign(headers, { [key]: this._headers[key].toString() }), - {} - ); // return all headers + (headers, key) => + Object.assign(headers, { [key]: this._headers[key].toString() }), + {} + ); // return all headers return asArr ? this._headers[key.toLowerCase()] : this._headers[key.toLowerCase()] - ? this._headers[key.toLowerCase()].toString() - : undefined; + ? this._headers[key.toLowerCase()].toString() + : undefined; } // Issue #130 @@ -131,9 +131,9 @@ class RESPONSE { this.header('Content-Type', 'application/json').send( (cb ? cb.replace(' ', '_') : 'callback') + - '(' + - this._serializer(body) + - ')' + '(' + + this._serializer(body) + + ')' ); } @@ -193,8 +193,8 @@ class RESPONSE { typeof expires === 'function' ? expires : typeof callback === 'function' - ? callback - : (e) => { + ? callback + : (e) => { if (e) this.error(e); }; @@ -236,10 +236,10 @@ class RESPONSE { cookieString += opts.maxAge && !isNaN(opts.maxAge) ? '; MaxAge=' + - ((opts.maxAge / 1000) | 0) + - (!opts.expires - ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() - : '') + ((opts.maxAge / 1000) | 0) + + (!opts.expires + ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() + : '') : ''; // path (String): Path for the cookie @@ -248,14 +248,14 @@ 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 ? '; SameSite=' + - (opts.sameSite === true - ? 'Strict' - : opts.sameSite === false + (opts.sameSite === true + ? 'Strict' + : opts.sameSite === false ? 'Lax' : opts.sameSite) : ''; @@ -323,7 +323,7 @@ class RESPONSE { let buffer, modified; let opts = typeof options === 'object' ? options : {}; - let fn = typeof callback === 'function' ? callback : () => {}; + let fn = typeof callback === 'function' ? callback : () => { }; // Add optional parameter support if (typeof options === 'function') { @@ -440,16 +440,16 @@ class RESPONSE { opts.methods ? opts.methods : acam - ? acam - : 'GET, PUT, POST, DELETE, OPTIONS' + ? acam + : 'GET, PUT, POST, DELETE, OPTIONS' ); this.header( 'Access-Control-Allow-Headers', opts.headers ? opts.headers : acah - ? acah - : 'Content-Type, Authorization, Content-Length, X-Requested-With' + ? acah + : 'Content-Type, Authorization, Content-Length, X-Requested-With' ); // Optional CORS headers @@ -500,8 +500,8 @@ class RESPONSE { date && typeof date.toUTCString === 'function' ? date : date && Date.parse(date) - ? new Date(date) - : new Date(); + ? new Date(date) + : new Date(); this.header('Last-Modified', lastModified.toUTCString()); } return this; @@ -559,10 +559,10 @@ class RESPONSE { }, this._request.interface === 'alb' ? { - statusDescription: `${this._statusCode} ${UTILS.statusLookup( - this._statusCode - )}`, - } + statusDescription: `${this._statusCode} ${UTILS.statusLookup( + this._statusCode + )}`, + } : {} ); @@ -596,13 +596,11 @@ class RESPONSE { error(code, e, 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 errorDetail = typeof code !== 'number' && e !== undefined ? e : detail; - const errorToSend = - typeof message === 'string' - ? new ResponseError(message, statusCode, errorDetail) - : message; + const errorToSend = typeof message === 'string' + ? new ApiError(message, statusCode, errorDetail) + : message; this.app.catchErrors(errorToSend, this, statusCode, errorDetail); } // end error From 01c774b011db97367efade1ddb72f7140076dd3f Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 5 Apr 2025 19:52:49 +0300 Subject: [PATCH 10/17] . --- index.js | 293 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 150 insertions(+), 143 deletions(-) diff --git a/index.js b/index.js index 6bee7c6..fefb82a 100644 --- a/index.js +++ b/index.js @@ -366,178 +366,185 @@ class API { } } + // If first time through, process error middleware if (response._state === 'processing') { + // Flag error state (this will avoid infinite error loops) response._state = 'error'; + + // Execute error middleware for (const err of this._errors) { if (response._state === 'done') break; - await new Promise(async (r) => { - let rtn = await err(e, response._request, response, () => { - r(); - }); - if (rtn) response.send(rtn); + // Promisify error middleware + // TODO: using async within a promise is an antipattern, therefore we need to refactor this asap + // eslint-disable-next-line no-async-promise-executorawait new Promise(async (r) => { + let rtn = await err(e, response._request, response, () => { r(); }); - } + if (rtn) response.send(rtn); + r(); + }); } + } - if (response._state !== 'done') response.json({ error: message }); + // Throw standard error unless callback has already been executed + if(response._state !== 'done') response.json({ error: message }); } // end catch // Custom callback async _callback(err, res, response) { - // Set done status - response._state = 'done'; - - // Execute finally - await this._finally(response._request, response); - - // Output logs - response._request._logs.forEach((log) => { - this._logger.logger( - JSON.stringify( - this._logger.detail - ? this._logger.format(log, response._request, response) - : log - ) - ); - }); - - // Generate access log - if ( - (this._logger.access || response._request._logs.length > 0) && - this._logger.access !== 'never' - ) { - let access = Object.assign( - this._logger.log( - 'access', - undefined, - response._request, - response._request.context - ), - { - statusCode: res.statusCode, - coldStart: response._request.coldStart, - count: response._request.requestCount, - } - ); - this._logger.logger( - JSON.stringify(this._logger.format(access, response._request, response)) - ); - } - - // Reset global error code - this._errorStatus = 500; - - // Execute the primary callback - typeof this._cb === 'function' && this._cb(err, res); - } // end _callback - - // Middleware handler - use(...args) { - // Extract routes - let routes = - typeof args[0] === 'string' - ? Array.of(args.shift()) - : Array.isArray(args[0]) - ? args.shift() - : ['/*']; - - // Init middleware stack - let middleware = []; - - // Add func args as middleware - for (let arg in args) { - if (typeof args[arg] === 'function') { - if (args[arg].length === 3) { - middleware.push(args[arg]); - } else if (args[arg].length === 4) { - this._errors.push(args[arg]); - } else { - throw new ConfigurationError( - 'Middleware must have 3 or 4 parameters' - ); - } + // Set done status + response._state = 'done'; + + // Execute finally + await this._finally(response._request, response); + + // Output logs + response._request._logs.forEach((log) => { + this._logger.logger( + JSON.stringify( + this._logger.detail + ? this._logger.format(log, response._request, response) + : log + ) + ); + }); + + // Generate access log + if ( + (this._logger.access || response._request._logs.length > 0) && + this._logger.access !== 'never' + ) { + let access = Object.assign( + this._logger.log( + 'access', + undefined, + response._request, + response._request.context + ), + { + statusCode: res.statusCode, + coldStart: response._request.coldStart, + count: response._request.requestCount, } - } + ); + this._logger.logger( + JSON.stringify(this._logger.format(access, response._request, response)) + ); + } - // Add middleware for all methods - if (middleware.length > 0) { - routes.forEach((route) => { - this.METHOD('__MW__', route, ...middleware); - }); + // Reset global error code + this._errorStatus = 500; + + // Execute the primary callback + typeof this._cb === 'function' && this._cb(err, res); +} // end _callback + +// Middleware handler +use(...args) { + // Extract routes + let routes = + typeof args[0] === 'string' + ? Array.of(args.shift()) + : Array.isArray(args[0]) + ? args.shift() + : ['/*']; + + // Init middleware stack + let middleware = []; + + // Add func args as middleware + for (let arg in args) { + if (typeof args[arg] === 'function') { + if (args[arg].length === 3) { + middleware.push(args[arg]); + } else if (args[arg].length === 4) { + this._errors.push(args[arg]); + } else { + throw new ConfigurationError( + 'Middleware must have 3 or 4 parameters' + ); + } } - } // end use - - // Finally handler - finally(fn) { - this._finally = fn; } - //-------------------------------------------------------------------------// - // UTILITY FUNCTIONS - //-------------------------------------------------------------------------// - - parseRoute(path) { - return path - .trim() - .replace(/^\/(.*?)(\/)*$/, '$1') - .split('/') - .filter((x) => x.trim() !== ''); + // Add middleware for all methods + if (middleware.length > 0) { + routes.forEach((route) => { + this.METHOD('__MW__', route, ...middleware); + }); } +} // end use - // Load app packages - app(packages) { - // Check for supplied packages - if (typeof packages === 'object') { - // Loop through and set package namespaces - for (let namespace in packages) { - try { - this._app[namespace] = packages[namespace]; - } catch (e) { - console.error(e.message); // eslint-disable-line no-console - } + // Finally handler + finally(fn) { + this._finally = fn; +} + +//-------------------------------------------------------------------------// +// UTILITY FUNCTIONS +//-------------------------------------------------------------------------// + +parseRoute(path) { + return path + .trim() + .replace(/^\/(.*?)(\/)*$/, '$1') + .split('/') + .filter((x) => x.trim() !== ''); +} + +// Load app packages +app(packages) { + // Check for supplied packages + if (typeof packages === 'object') { + // Loop through and set package namespaces + for (let namespace in packages) { + try { + this._app[namespace] = packages[namespace]; + } catch (e) { + console.error(e.message); // eslint-disable-line no-console } - } else if (arguments.length === 2 && typeof packages === 'string') { - this._app[packages] = arguments[1]; - } // end if + } + } else if (arguments.length === 2 && typeof packages === 'string') { + this._app[packages] = arguments[1]; + } // end if - // Return a reference - return this._app; - } + // Return a reference + return this._app; +} - // Register routes with options - register(fn, opts) { - let options = typeof opts === 'object' ? opts : {}; +// Register routes with options +register(fn, opts) { + let options = typeof opts === 'object' ? opts : {}; - // Extract Prefix - let prefix = - options.prefix && options.prefix.toString().trim() !== '' - ? this.parseRoute(options.prefix) - : []; + // Extract Prefix + let prefix = + options.prefix && options.prefix.toString().trim() !== '' + ? this.parseRoute(options.prefix) + : []; - // Concat to existing prefix - this._prefix = this._prefix.concat(prefix); + // Concat to existing prefix + this._prefix = this._prefix.concat(prefix); - // Execute the routing function - fn(this, options); + // Execute the routing function + fn(this, options); - // Remove the last prefix (if a prefix exists) - if (prefix.length > 0) { - this._prefix = this._prefix.slice(0, -prefix.length); - } - } // end register + // Remove the last prefix (if a prefix exists) + if (prefix.length > 0) { + this._prefix = this._prefix.slice(0, -prefix.length); + } +} // end register - // prettyPrint debugger - routes(format) { - // Parse the routes - let routes = UTILS.extractRoutes(this._routes); +// prettyPrint debugger +routes(format) { + // Parse the routes + let routes = UTILS.extractRoutes(this._routes); - if (format) { - console.log(prettyPrint(routes)); // eslint-disable-line no-console - } else { - return routes; - } + if (format) { + console.log(prettyPrint(routes)); // eslint-disable-line no-console + } else { + return routes; } +} } // end API class // Export the API class as a new instance From e5b8d27268da7628612b0efd497eccc8bb88fba8 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 5 Apr 2025 19:57:07 +0300 Subject: [PATCH 11/17] . --- index.js | 289 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 145 insertions(+), 144 deletions(-) diff --git a/index.js b/index.js index fefb82a..bf7fd70 100644 --- a/index.js +++ b/index.js @@ -376,175 +376,176 @@ class API { if (response._state === 'done') break; // Promisify error middleware // TODO: using async within a promise is an antipattern, therefore we need to refactor this asap - // eslint-disable-next-line no-async-promise-executorawait new Promise(async (r) => { - let rtn = await err(e, response._request, response, () => { + // eslint-disable-next-line no-async-promise-executor + await new Promise(async (r) => { + let rtn = await err(e, response._request, response, () => { + r(); + }); + if (rtn) response.send(rtn); r(); }); - if (rtn) response.send(rtn); - r(); - }); + } } - } - // Throw standard error unless callback has already been executed - if(response._state !== 'done') response.json({ error: message }); + // Throw standard error unless callback has already been executed + if (response._state !== 'done') response.json({ error: message }); } // end catch // Custom callback async _callback(err, res, response) { - // Set done status - response._state = 'done'; - - // Execute finally - await this._finally(response._request, response); - - // Output logs - response._request._logs.forEach((log) => { - this._logger.logger( - JSON.stringify( - this._logger.detail - ? this._logger.format(log, response._request, response) - : log - ) - ); - }); - - // Generate access log - if ( - (this._logger.access || response._request._logs.length > 0) && - this._logger.access !== 'never' - ) { - let access = Object.assign( - this._logger.log( - 'access', - undefined, - response._request, - response._request.context - ), - { - statusCode: res.statusCode, - coldStart: response._request.coldStart, - count: response._request.requestCount, - } - ); - this._logger.logger( - JSON.stringify(this._logger.format(access, response._request, response)) - ); - } + // Set done status + response._state = 'done'; + + // Execute finally + await this._finally(response._request, response); + + // Output logs + response._request._logs.forEach((log) => { + this._logger.logger( + JSON.stringify( + this._logger.detail + ? this._logger.format(log, response._request, response) + : log + ) + ); + }); + + // Generate access log + if ( + (this._logger.access || response._request._logs.length > 0) && + this._logger.access !== 'never' + ) { + let access = Object.assign( + this._logger.log( + 'access', + undefined, + response._request, + response._request.context + ), + { + statusCode: res.statusCode, + coldStart: response._request.coldStart, + count: response._request.requestCount, + } + ); + this._logger.logger( + JSON.stringify(this._logger.format(access, response._request, response)) + ); + } + + // Reset global error code + this._errorStatus = 500; - // Reset global error code - this._errorStatus = 500; - - // Execute the primary callback - typeof this._cb === 'function' && this._cb(err, res); -} // end _callback - -// Middleware handler -use(...args) { - // Extract routes - let routes = - typeof args[0] === 'string' - ? Array.of(args.shift()) - : Array.isArray(args[0]) - ? args.shift() - : ['/*']; - - // Init middleware stack - let middleware = []; - - // Add func args as middleware - for (let arg in args) { - if (typeof args[arg] === 'function') { - if (args[arg].length === 3) { - middleware.push(args[arg]); - } else if (args[arg].length === 4) { - this._errors.push(args[arg]); - } else { - throw new ConfigurationError( - 'Middleware must have 3 or 4 parameters' - ); + // Execute the primary callback + typeof this._cb === 'function' && this._cb(err, res); + } // end _callback + + // Middleware handler + use(...args) { + // Extract routes + let routes = + typeof args[0] === 'string' + ? Array.of(args.shift()) + : Array.isArray(args[0]) + ? args.shift() + : ['/*']; + + // Init middleware stack + let middleware = []; + + // Add func args as middleware + for (let arg in args) { + if (typeof args[arg] === 'function') { + if (args[arg].length === 3) { + middleware.push(args[arg]); + } else if (args[arg].length === 4) { + this._errors.push(args[arg]); + } else { + throw new ConfigurationError( + 'Middleware must have 3 or 4 parameters' + ); + } } } - } - // Add middleware for all methods - if (middleware.length > 0) { - routes.forEach((route) => { - this.METHOD('__MW__', route, ...middleware); - }); - } -} // end use + // Add middleware for all methods + if (middleware.length > 0) { + routes.forEach((route) => { + this.METHOD('__MW__', route, ...middleware); + }); + } + } // end use // Finally handler finally(fn) { - this._finally = fn; -} - -//-------------------------------------------------------------------------// -// UTILITY FUNCTIONS -//-------------------------------------------------------------------------// - -parseRoute(path) { - return path - .trim() - .replace(/^\/(.*?)(\/)*$/, '$1') - .split('/') - .filter((x) => x.trim() !== ''); -} - -// Load app packages -app(packages) { - // Check for supplied packages - if (typeof packages === 'object') { - // Loop through and set package namespaces - for (let namespace in packages) { - try { - this._app[namespace] = packages[namespace]; - } catch (e) { - console.error(e.message); // eslint-disable-line no-console + this._finally = fn; + } + + //-------------------------------------------------------------------------// + // UTILITY FUNCTIONS + //-------------------------------------------------------------------------// + + parseRoute(path) { + return path + .trim() + .replace(/^\/(.*?)(\/)*$/, '$1') + .split('/') + .filter((x) => x.trim() !== ''); + } + + // Load app packages + app(packages) { + // Check for supplied packages + if (typeof packages === 'object') { + // Loop through and set package namespaces + for (let namespace in packages) { + try { + this._app[namespace] = packages[namespace]; + } catch (e) { + console.error(e.message); // eslint-disable-line no-console + } } - } - } else if (arguments.length === 2 && typeof packages === 'string') { - this._app[packages] = arguments[1]; - } // end if + } else if (arguments.length === 2 && typeof packages === 'string') { + this._app[packages] = arguments[1]; + } // end if - // Return a reference - return this._app; -} + // Return a reference + return this._app; + } -// Register routes with options -register(fn, opts) { - let options = typeof opts === 'object' ? opts : {}; + // Register routes with options + register(fn, opts) { + let options = typeof opts === 'object' ? opts : {}; - // Extract Prefix - let prefix = - options.prefix && options.prefix.toString().trim() !== '' - ? this.parseRoute(options.prefix) - : []; + // Extract Prefix + let prefix = + options.prefix && options.prefix.toString().trim() !== '' + ? this.parseRoute(options.prefix) + : []; - // Concat to existing prefix - this._prefix = this._prefix.concat(prefix); + // Concat to existing prefix + this._prefix = this._prefix.concat(prefix); - // Execute the routing function - fn(this, options); + // Execute the routing function + fn(this, options); - // Remove the last prefix (if a prefix exists) - if (prefix.length > 0) { - this._prefix = this._prefix.slice(0, -prefix.length); - } -} // end register + // Remove the last prefix (if a prefix exists) + if (prefix.length > 0) { + this._prefix = this._prefix.slice(0, -prefix.length); + } + } // end register -// prettyPrint debugger -routes(format) { - // Parse the routes - let routes = UTILS.extractRoutes(this._routes); + // prettyPrint debugger + routes(format) { + // Parse the routes + let routes = UTILS.extractRoutes(this._routes); - if (format) { - console.log(prettyPrint(routes)); // eslint-disable-line no-console - } else { - return routes; + if (format) { + console.log(prettyPrint(routes)); // eslint-disable-line no-console + } else { + return routes; + } } -} } // end API class // Export the API class as a new instance From 5816ed5c4ef84c50ff4cd2cc8cbd65949f387741 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 5 Apr 2025 19:58:44 +0300 Subject: [PATCH 12/17] . --- __tests__/errorHandling.unit.js | 46 --------------------------------- 1 file changed, 46 deletions(-) diff --git a/__tests__/errorHandling.unit.js b/__tests__/errorHandling.unit.js index 8dec498..0bbbfcd 100644 --- a/__tests__/errorHandling.unit.js +++ b/__tests__/errorHandling.unit.js @@ -328,52 +328,6 @@ describe('Error Handling Tests:', function () { expect(error.name).toEqual('ConfigurationError') }) // end it - it('ResponseError 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('ResponseError 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('ResponseError 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('ResponseError properties', function () { - const error = new errors.ResponseError('test message', 403, { foo: 'bar' }); - expect(error.name).toBe('ResponseError'); - expect(error.message).toBe('test message'); - expect(error.code).toBe(403); - expect(error.detail).toEqual({ foo: 'bar' }); - }); - - it('ResponseError default code', function () { - const error = new errors.ResponseError('test message'); - expect(error.code).toBe(500); - }); - 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) })); From 66fb2a0424724fda576114bcfaec4d2056997a3b Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 5 Apr 2025 20:11:42 +0300 Subject: [PATCH 13/17] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) => { From d50c1fbc53a962fed49e5ae5e98e40abf610ff34 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 5 Apr 2025 20:11:57 +0300 Subject: [PATCH 14/17] prettier --- index.js | 14 +++++----- lib/errors.js | 2 +- lib/response.js | 68 +++++++++++++++++++++++++------------------------ 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/index.js b/index.js index bf7fd70..3f6fdb1 100644 --- a/index.js +++ b/index.js @@ -42,8 +42,8 @@ class API { : {}; this._compression = props && - (typeof props.compression === 'boolean' || - Array.isArray(props.compression)) + (typeof props.compression === 'boolean' || + Array.isArray(props.compression)) ? props.compression : false; @@ -84,7 +84,7 @@ class API { this._app = {}; // Executed after the callback - this._finally = () => { }; + this._finally = () => {}; // Global error status (used for response parsing errors) this._errorStatus = 500; @@ -213,8 +213,8 @@ class API { stack: _stack['m'][method] ? _stack['m'][method].concat(stack) : _stack['*'][method] - ? _stack['*'][method].concat(stack) - : stack, + ? _stack['*'][method].concat(stack) + : stack, // inherited: _stack[method] ? _stack[method] : [], route: '/' + parsedPath.join('/'), path: '/' + this._prefix.concat(parsedPath).join('/'), @@ -447,8 +447,8 @@ class API { typeof args[0] === 'string' ? Array.of(args.shift()) : Array.isArray(args[0]) - ? args.shift() - : ['/*']; + ? args.shift() + : ['/*']; // Init middleware stack let middleware = []; diff --git a/lib/errors.js b/lib/errors.js index 7d05846..e4baaa7 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -67,5 +67,5 @@ module.exports = { ConfigurationError, ResponseError, ApiError, - FileError + FileError, }; diff --git a/lib/response.js b/lib/response.js index ea1e987..3986d88 100644 --- a/lib/response.js +++ b/lib/response.js @@ -85,15 +85,15 @@ class RESPONSE { return asArr ? this._headers : Object.keys(this._headers).reduce( - (headers, key) => - Object.assign(headers, { [key]: this._headers[key].toString() }), - {} - ); // return all headers + (headers, key) => + Object.assign(headers, { [key]: this._headers[key].toString() }), + {} + ); // return all headers return asArr ? this._headers[key.toLowerCase()] : this._headers[key.toLowerCase()] - ? this._headers[key.toLowerCase()].toString() - : undefined; + ? this._headers[key.toLowerCase()].toString() + : undefined; } // Issue #130 @@ -131,9 +131,9 @@ class RESPONSE { this.header('Content-Type', 'application/json').send( (cb ? cb.replace(' ', '_') : 'callback') + - '(' + - this._serializer(body) + - ')' + '(' + + this._serializer(body) + + ')' ); } @@ -193,8 +193,8 @@ class RESPONSE { typeof expires === 'function' ? expires : typeof callback === 'function' - ? callback - : (e) => { + ? callback + : (e) => { if (e) this.error(e); }; @@ -236,10 +236,10 @@ class RESPONSE { cookieString += opts.maxAge && !isNaN(opts.maxAge) ? '; MaxAge=' + - ((opts.maxAge / 1000) | 0) + - (!opts.expires - ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() - : '') + ((opts.maxAge / 1000) | 0) + + (!opts.expires + ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() + : '') : ''; // path (String): Path for the cookie @@ -253,9 +253,9 @@ class RESPONSE { cookieString += opts.sameSite !== undefined ? '; SameSite=' + - (opts.sameSite === true - ? 'Strict' - : opts.sameSite === false + (opts.sameSite === true + ? 'Strict' + : opts.sameSite === false ? 'Lax' : opts.sameSite) : ''; @@ -323,7 +323,7 @@ class RESPONSE { let buffer, modified; let opts = typeof options === 'object' ? options : {}; - let fn = typeof callback === 'function' ? callback : () => { }; + let fn = typeof callback === 'function' ? callback : () => {}; // Add optional parameter support if (typeof options === 'function') { @@ -440,16 +440,16 @@ class RESPONSE { opts.methods ? opts.methods : acam - ? acam - : 'GET, PUT, POST, DELETE, OPTIONS' + ? acam + : 'GET, PUT, POST, DELETE, OPTIONS' ); this.header( 'Access-Control-Allow-Headers', opts.headers ? opts.headers : acah - ? acah - : 'Content-Type, Authorization, Content-Length, X-Requested-With' + ? acah + : 'Content-Type, Authorization, Content-Length, X-Requested-With' ); // Optional CORS headers @@ -500,8 +500,8 @@ class RESPONSE { date && typeof date.toUTCString === 'function' ? date : date && Date.parse(date) - ? new Date(date) - : new Date(); + ? new Date(date) + : new Date(); this.header('Last-Modified', lastModified.toUTCString()); } return this; @@ -559,10 +559,10 @@ class RESPONSE { }, this._request.interface === 'alb' ? { - statusDescription: `${this._statusCode} ${UTILS.statusLookup( - this._statusCode - )}`, - } + statusDescription: `${this._statusCode} ${UTILS.statusLookup( + this._statusCode + )}`, + } : {} ); @@ -596,11 +596,13 @@ class RESPONSE { error(code, e, 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 errorDetail = + typeof code !== 'number' && e !== undefined ? e : detail; - const errorToSend = typeof message === 'string' - ? new ApiError(message, statusCode, errorDetail) - : message; + const errorToSend = + typeof message === 'string' + ? new ApiError(message, statusCode, errorDetail) + : message; this.app.catchErrors(errorToSend, this, statusCode, errorDetail); } // end error From 6c0ceba3b164038029cab380e6d064c9ccc4d2cf Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 5 Apr 2025 20:14:51 +0300 Subject: [PATCH 15/17] type test --- index.test-d.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) 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); From d84db0b9281df8abf262e19463e9b3777e3d065e Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 5 Apr 2025 20:17:56 +0300 Subject: [PATCH 16/17] lint --- index.js | 2 +- lib/response.js | 58 ++++++++++++++++++++++++------------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/index.js b/index.js index 3f6fdb1..58a544d 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,7 @@ const RESPONSE = require('./lib/response'); const UTILS = require('./lib/utils'); const LOGGER = require('./lib/logger'); const S3 = () => require('./lib/s3-service'); -const { ResponseError, ConfigurationError, ApiError } = require('./lib/errors'); +const { ConfigurationError, ApiError } = require('./lib/errors'); const prettyPrint = require('./lib/prettyPrint'); class API { diff --git a/lib/response.js b/lib/response.js index 3986d88..9394ed6 100644 --- a/lib/response.js +++ b/lib/response.js @@ -85,15 +85,15 @@ class RESPONSE { return asArr ? this._headers : Object.keys(this._headers).reduce( - (headers, key) => - Object.assign(headers, { [key]: this._headers[key].toString() }), - {} - ); // return all headers + (headers, key) => + Object.assign(headers, { [key]: this._headers[key].toString() }), + {} + ); // return all headers return asArr ? this._headers[key.toLowerCase()] : this._headers[key.toLowerCase()] - ? this._headers[key.toLowerCase()].toString() - : undefined; + ? this._headers[key.toLowerCase()].toString() + : undefined; } // Issue #130 @@ -131,9 +131,9 @@ class RESPONSE { this.header('Content-Type', 'application/json').send( (cb ? cb.replace(' ', '_') : 'callback') + - '(' + - this._serializer(body) + - ')' + '(' + + this._serializer(body) + + ')' ); } @@ -193,8 +193,8 @@ class RESPONSE { typeof expires === 'function' ? expires : typeof callback === 'function' - ? callback - : (e) => { + ? callback + : (e) => { if (e) this.error(e); }; @@ -236,10 +236,10 @@ class RESPONSE { cookieString += opts.maxAge && !isNaN(opts.maxAge) ? '; MaxAge=' + - ((opts.maxAge / 1000) | 0) + - (!opts.expires - ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() - : '') + ((opts.maxAge / 1000) | 0) + + (!opts.expires + ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() + : '') : ''; // path (String): Path for the cookie @@ -253,9 +253,9 @@ class RESPONSE { cookieString += opts.sameSite !== undefined ? '; SameSite=' + - (opts.sameSite === true - ? 'Strict' - : opts.sameSite === false + (opts.sameSite === true + ? 'Strict' + : opts.sameSite === false ? 'Lax' : opts.sameSite) : ''; @@ -323,7 +323,7 @@ class RESPONSE { let buffer, modified; let opts = typeof options === 'object' ? options : {}; - let fn = typeof callback === 'function' ? callback : () => {}; + let fn = typeof callback === 'function' ? callback : () => { }; // Add optional parameter support if (typeof options === 'function') { @@ -440,16 +440,16 @@ class RESPONSE { opts.methods ? opts.methods : acam - ? acam - : 'GET, PUT, POST, DELETE, OPTIONS' + ? acam + : 'GET, PUT, POST, DELETE, OPTIONS' ); this.header( 'Access-Control-Allow-Headers', opts.headers ? opts.headers : acah - ? acah - : 'Content-Type, Authorization, Content-Length, X-Requested-With' + ? acah + : 'Content-Type, Authorization, Content-Length, X-Requested-With' ); // Optional CORS headers @@ -500,8 +500,8 @@ class RESPONSE { date && typeof date.toUTCString === 'function' ? date : date && Date.parse(date) - ? new Date(date) - : new Date(); + ? new Date(date) + : new Date(); this.header('Last-Modified', lastModified.toUTCString()); } return this; @@ -559,10 +559,10 @@ class RESPONSE { }, this._request.interface === 'alb' ? { - statusDescription: `${this._statusCode} ${UTILS.statusLookup( - this._statusCode - )}`, - } + statusDescription: `${this._statusCode} ${UTILS.statusLookup( + this._statusCode + )}`, + } : {} ); From 5124ea6af318e1b9f3dee86a33a1b93a60269732 Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 5 Apr 2025 20:19:57 +0300 Subject: [PATCH 17/17] prettier --- lib/response.js | 58 ++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/response.js b/lib/response.js index 9394ed6..3986d88 100644 --- a/lib/response.js +++ b/lib/response.js @@ -85,15 +85,15 @@ class RESPONSE { return asArr ? this._headers : Object.keys(this._headers).reduce( - (headers, key) => - Object.assign(headers, { [key]: this._headers[key].toString() }), - {} - ); // return all headers + (headers, key) => + Object.assign(headers, { [key]: this._headers[key].toString() }), + {} + ); // return all headers return asArr ? this._headers[key.toLowerCase()] : this._headers[key.toLowerCase()] - ? this._headers[key.toLowerCase()].toString() - : undefined; + ? this._headers[key.toLowerCase()].toString() + : undefined; } // Issue #130 @@ -131,9 +131,9 @@ class RESPONSE { this.header('Content-Type', 'application/json').send( (cb ? cb.replace(' ', '_') : 'callback') + - '(' + - this._serializer(body) + - ')' + '(' + + this._serializer(body) + + ')' ); } @@ -193,8 +193,8 @@ class RESPONSE { typeof expires === 'function' ? expires : typeof callback === 'function' - ? callback - : (e) => { + ? callback + : (e) => { if (e) this.error(e); }; @@ -236,10 +236,10 @@ class RESPONSE { cookieString += opts.maxAge && !isNaN(opts.maxAge) ? '; MaxAge=' + - ((opts.maxAge / 1000) | 0) + - (!opts.expires - ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() - : '') + ((opts.maxAge / 1000) | 0) + + (!opts.expires + ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() + : '') : ''; // path (String): Path for the cookie @@ -253,9 +253,9 @@ class RESPONSE { cookieString += opts.sameSite !== undefined ? '; SameSite=' + - (opts.sameSite === true - ? 'Strict' - : opts.sameSite === false + (opts.sameSite === true + ? 'Strict' + : opts.sameSite === false ? 'Lax' : opts.sameSite) : ''; @@ -323,7 +323,7 @@ class RESPONSE { let buffer, modified; let opts = typeof options === 'object' ? options : {}; - let fn = typeof callback === 'function' ? callback : () => { }; + let fn = typeof callback === 'function' ? callback : () => {}; // Add optional parameter support if (typeof options === 'function') { @@ -440,16 +440,16 @@ class RESPONSE { opts.methods ? opts.methods : acam - ? acam - : 'GET, PUT, POST, DELETE, OPTIONS' + ? acam + : 'GET, PUT, POST, DELETE, OPTIONS' ); this.header( 'Access-Control-Allow-Headers', opts.headers ? opts.headers : acah - ? acah - : 'Content-Type, Authorization, Content-Length, X-Requested-With' + ? acah + : 'Content-Type, Authorization, Content-Length, X-Requested-With' ); // Optional CORS headers @@ -500,8 +500,8 @@ class RESPONSE { date && typeof date.toUTCString === 'function' ? date : date && Date.parse(date) - ? new Date(date) - : new Date(); + ? new Date(date) + : new Date(); this.header('Last-Modified', lastModified.toUTCString()); } return this; @@ -559,10 +559,10 @@ class RESPONSE { }, this._request.interface === 'alb' ? { - statusDescription: `${this._statusCode} ${UTILS.statusLookup( - this._statusCode - )}`, - } + statusDescription: `${this._statusCode} ${UTILS.statusLookup( + this._statusCode + )}`, + } : {} );