From 0db7c896cfe5a2b074f96c852645a35d6f24752b Mon Sep 17 00:00:00 2001 From: Adam Fanello Date: Mon, 27 Feb 2023 11:42:33 -0800 Subject: [PATCH] Small updates to LambdaUtils result handling Issue #138 - Results missing content-type header Issue #139 - Unhandled exception should return root cause Bumped to v5.1.0 --- docs/types/handler-utils.d.ts | 2 +- lambda-utils/lib/handler-utils.test.ts | 46 +++++++++++++++++-- lambda-utils/lib/handler-utils.ts | 12 +++-- .../lib/resolved-promise-is-success.ts | 5 +- lambda-utils/lib/unhandled-exception.ts | 25 ++++++++-- lambda-utils/package-lock.json | 2 +- lambda-utils/package.json | 2 +- 7 files changed, 79 insertions(+), 15 deletions(-) diff --git a/docs/types/handler-utils.d.ts b/docs/types/handler-utils.d.ts index 44f3afa..60b8a0c 100644 --- a/docs/types/handler-utils.d.ts +++ b/docs/types/handler-utils.d.ts @@ -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 diff --git a/lambda-utils/lib/handler-utils.test.ts b/lambda-utils/lib/handler-utils.test.ts index 9d559eb..a4c868f 100644 --- a/lambda-utils/lib/handler-utils.test.ts +++ b/lambda-utils/lib/handler-utils.test.ts @@ -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); @@ -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 () => { @@ -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 () => { @@ -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 () => { @@ -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 => { + // 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" + } }); }); }); diff --git a/lambda-utils/lib/handler-utils.ts b/lambda-utils/lib/handler-utils.ts index 5998276..f49c267 100644 --- a/lambda-utils/lib/handler-utils.ts +++ b/lambda-utils/lib/handler-utils.ts @@ -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 @@ -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); diff --git a/lambda-utils/lib/resolved-promise-is-success.ts b/lambda-utils/lib/resolved-promise-is-success.ts index 9c70681..ff433a8 100644 --- a/lambda-utils/lib/resolved-promise-is-success.ts +++ b/lambda-utils/lib/resolved-promise-is-success.ts @@ -13,7 +13,10 @@ export const resolvedPromiseIsSuccessMiddleware = (): middy.MiddlewareObj => ({ 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; + } +} diff --git a/lambda-utils/package-lock.json b/lambda-utils/package-lock.json index 6ec7712..ba91697 100644 --- a/lambda-utils/package-lock.json +++ b/lambda-utils/package-lock.json @@ -1,6 +1,6 @@ { "name": "@sailplane/lambda-utils", - "version": "5.0.0", + "version": "5.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/lambda-utils/package.json b/lambda-utils/package.json index 93a965b..b2979f8 100644 --- a/lambda-utils/package.json +++ b/lambda-utils/package.json @@ -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",