Skip to content

fix(error-handling-middleware): handle str error as ApiError #287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 5, 2025
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
312 changes: 179 additions & 133 deletions __tests__/errorHandling.unit.js

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
15 changes: 6 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = {
Expand All @@ -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);
}
Expand All @@ -387,7 +384,7 @@ class API {
if (rtn) response.send(rtn);
r();
});
} // end for
}
}

// Throw standard error unless callback has already been executed
Expand Down
10 changes: 10 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ConfigurationError,
ResponseError,
FileError,
ApiError,
} from './index';
import {
APIGatewayProxyEvent,
Expand Down Expand Up @@ -255,3 +256,12 @@ const fileError = new FileError('File not found', {
syscall: 'open',
});
expectType<FileError>(fileError);
expectType<string>(fileError.message);
expectType<string>(fileError.name);
expectType<string | undefined>(fileError.stack);

const apiError = new ApiError('Api error', 500);
expectType<ApiError>(apiError);
expectType<string>(apiError.message);
expectType<number | undefined>(apiError.code);
expectType<any>(apiError.detail);
12 changes: 12 additions & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -55,5 +66,6 @@ module.exports = {
MethodError,
ConfigurationError,
ResponseError,
ApiError,
FileError,
};
19 changes: 13 additions & 6 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down