Skip to content

Commit

Permalink
Small updates to LambdaUtils result handling
Browse files Browse the repository at this point in the history
Issue #138 - Results missing content-type header
Issue #139 - Unhandled exception should return root cause
Bumped to v5.1.0
  • Loading branch information
kernwig committed Feb 27, 2023
1 parent 29605ab commit 0db7c89
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 15 deletions.
2 changes: 1 addition & 1 deletion docs/types/handler-utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export declare function apiSuccess(result?: any): APIGatewayProxyResult;
/**
* Construct the object that API Gateway payload format v1 wants back upon a failed run.
*
* Often, it is simpler to throw an http-errors exception from your #wrapApiHandler
* Often, it is simpler to throw a http-errors exception from your #wrapApiHandler
* handler.
*
* @see https://www.npmjs.com/package/http-errors
Expand Down
46 changes: 42 additions & 4 deletions lambda-utils/lib/handler-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ describe("LambdaUtils", () => {

// THEN

// CORS header set in response
// Headers set in response
expect(response.headers?.['Access-Control-Allow-Origin']).toEqual('*');
expect(response.headers?.['content-type']).toEqual("application/json; charset=utf-8");

const resultEvent: APIGatewayProxyEvent = JSON.parse(response.body);

Expand Down Expand Up @@ -113,6 +114,7 @@ describe("LambdaUtils", () => {
expect(response.statusCode).toEqual(200);
expect(response.body).toEqual("{\"message\":\"Hello\"}");
expect(response.headers?.["Access-Control-Allow-Origin"]).toEqual("*");
expect(response.headers?.['content-type']).toEqual("application/json; charset=utf-8");
});

test("wrapApiHandler promise empty success", async () => {
Expand Down Expand Up @@ -147,6 +149,7 @@ describe("LambdaUtils", () => {
expect(response.statusCode).toEqual(200);
expect(response.body).toBeFalsy();
expect(response.headers!["Access-Control-Allow-Origin"]).toEqual("*");
expect(response.headers?.['content-type']).toEqual("text/plain; charset=utf-8");
});

test("wrapApiHandler throw Error", async () => {
Expand All @@ -161,8 +164,13 @@ describe("LambdaUtils", () => {
) as APIGatewayProxyResult;

// THEN
expect(response.statusCode).toEqual(500);
expect(response.body).toEqual("Error: oops");
expect(response).toEqual({
statusCode: 500,
body: 'Error: oops',
headers: {
"content-type": "text/plain; charset=utf-8"
}
});
});

test("wrapApiHandlerV2 throw http-error", async () => {
Expand All @@ -179,7 +187,37 @@ describe("LambdaUtils", () => {
// THEN
expect(response).toEqual({
statusCode: 404,
body: 'NotFoundError: Not Found'
body: 'NotFoundError: Not Found',
headers: {
"content-type": "text/plain; charset=utf-8"
}
});
});

test("wrapApiHandlerV2 throw nested cause http-error", async () => {
// GIVEN
const handler = LambdaUtils.wrapApiHandlerV2(async (): Promise<APIGatewayProxyStructuredResultV2> => {
// The 'cause' option isn't gained until Node 16.9.0, but this library
// targets an older version in order to be backward compatible.
// So, we fake this.
// @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
const error: any = new Error("I'm confused");
error.cause = new createError.BadRequest();
throw error;
});

// WHEN
const response = await handler(
{} as unknown as APIGatewayProxyEventV2, mockContext as Context, {} as any
) as APIGatewayProxyResult;

// THEN
expect(response).toEqual({
statusCode: 400,
body: 'BadRequestError: Bad Request',
headers: {
"content-type": "text/plain; charset=utf-8"
}
});
});
});
Expand Down
12 changes: 9 additions & 3 deletions lambda-utils/lib/handler-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,17 @@ export function wrapApiHandlerV2(handler: AsyncProxyHandlerV2): AsyncMiddyifedHa
export function apiSuccess(result?: any): APIGatewayProxyResult {
return {
statusCode: 200,
body: result ? JSON.stringify(result) : ''
body: result ? JSON.stringify(result) : '',
headers: {
"content-type": result ? "application/json; charset=utf-8" : "text/plain; charset=utf-8"
}
};
}

/**
* Construct the object that API Gateway payload format v1 wants back upon a failed run.
*
* Often, it is simpler to throw an http-errors exception from your #wrapApiHandler
* Often, it is simpler to throw a http-errors exception from your #wrapApiHandler
* handler.
*
* @see https://www.npmjs.com/package/http-errors
Expand All @@ -108,7 +111,10 @@ export function apiSuccess(result?: any): APIGatewayProxyResult {
export function apiFailure(statusCode: number, message?: string): APIGatewayProxyResult {
const response = {
statusCode,
body: message || ''
body: message || '',
headers: {
"content-type": "text/plain; charset=utf-8"
}
};

logger.warn("Response to API Gateway: ", response);
Expand Down
5 changes: 4 additions & 1 deletion lambda-utils/lib/resolved-promise-is-success.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ export const resolvedPromiseIsSuccessMiddleware = (): middy.MiddlewareObj<APIGat
if (!response || typeof response !== 'object' || (!response.statusCode && !response.body)) {
request.response = {
statusCode: 200,
body: response ? JSON.stringify(response) : ''
body: response ? JSON.stringify(response) : '',
headers: {
"content-type": response ? "application/json; charset=utf-8" : "text/plain; charset=utf-8"
}
};
}
}
Expand Down
25 changes: 21 additions & 4 deletions lambda-utils/lib/unhandled-exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,36 @@ const logger = new Logger('lambda-utils');
* Middleware to handle any otherwise unhandled exception by logging it and generating
* an HTTP 500 response.
*
* Fine tuned to work better than the Middy version, and uses @sailplane/logger.
* Fine-tuned to work better than the Middy version, and uses @sailplane/logger.
*/
export const unhandledExceptionMiddleware = (): middy.MiddlewareObj<APIGatewayProxyEventAnyVersion, APIGatewayProxyResultAnyVersion> => ({
onError: async (request) => {
logger.error('Unhandled exception:', request.error);

request.response = request.response || {};
/* istanbul ignore else - nominal path is for response to be brand new*/
/* istanbul ignore else - nominal path is for response to be brand new */
if ((request.response.statusCode || 0) < 400) {
request.response.statusCode = (request.error as any)?.statusCode ?? 500;
request.response.body = request.error?.toString() ?? '';
const error = findRootCause(request.error);
request.response.statusCode = error?.statusCode || 500;
request.response.body = error?.toString() ?? '';
request.response.headers = request.response.headers ?? {};
request.response.headers["content-type"] = "text/plain; charset=utf-8";
}

logger.info("Response to API Gateway: ", request.response);
}
});

type ErrorWithStatusAndCause =
Error
& { statusCode?: number, cause?: ErrorWithStatusAndCause };

function findRootCause(error: ErrorWithStatusAndCause | null): ErrorWithStatusAndCause | null {
if (error?.statusCode && error.statusCode >= 400) {
return error;
} else if (error?.cause) {
return findRootCause(error.cause);
} else {
return error;
}
}
2 changes: 1 addition & 1 deletion lambda-utils/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lambda-utils/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sailplane/lambda-utils",
"version": "5.0.0",
"version": "5.1.0",
"description": "Use middleware to remove redundancy in AWS Lambda handlers.",
"keywords": [
"aws",
Expand Down

0 comments on commit 0db7c89

Please sign in to comment.