Repository: alexcasalboni/aws-lambda-power-tuning
Pull request #247 (2024-04-29 16:29:13)

Show error message and type on function invocation errors
Error type, message and trace will be shown for function invocation errors. Also when happens for pre or post processing.

Examples for executor function response

- Raising an exception in the function code:
```
{
  "errorType": "Error",
  "errorMessage": "Invocation error (running in series) with payload \"null\" - original error type: \"Exception\", original error message: \"Exception raised during execution.\", trace: \"  File \"/var/task/lambda_function.py\", line 9, in lambda_handler\n    raise Exception(\"Exception raised during execution.\")\n\"",
  "trace": [
    "Error: Invocation error (running in series) with payload \"null\" - original error type: \"Exception\", original error message: \"Exception raised during execution.\", trace: \"  File \"/var/task/lambda_function.py\", line 9, in lambda_handler",
    "    raise Exception(\"Exception raised during execution.\")",
    "\"",
    "    at handleLambdaInvocationError (/var/task/executor.js:177:11)",
    "    at runInSeries (/var/task/executor.js:160:13)",
    "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)",
    "    at async module.exports.handler (/var/task/executor.js:66:19)"
  ]
}
```

original function response:

```
{
    "StatusCode": 200,
    "FunctionError": "Unhandled",
    "ExecutedVersion": "$LATEST"
    "Payload": {"errorMessage": "Exception raised during execution.", "errorType": "Exception", "requestId": "c99e0a50-3b11-4c1a-aa20-108339da181b", "stackTrace": ["  File \"/var/task/lambda_function.py\", line 9, in lambda_handler\n    raise Exception(\"Exception raised during execution.\")\n"]}
}
```

- Timeout

```
{
  "errorType": "Error",
  "errorMessage": "Invocation error (running in series) with payload \"null\" - original error type: \"undefined\", original error message: \"2024-04-29T15:49:41.111Z 29faf2b8-bbed-49ed-8df8-31cfb63a73a5 Task timed out after 3.00 seconds\", trace: \"undefined\"",
  "trace": [
    "Error: Invocation error (running in series) with payload \"null\" - original error type: \"undefined\", original error message: \"2024-04-29T15:49:41.111Z 29faf2b8-bbed-49ed-8df8-31cfb63a73a5 Task timed out after 3.00 seconds\", trace: \"undefined\"",
    "    at handleLambdaInvocationError (/var/task/executor.js:177:11)",
    "    at runInSeries (/var/task/executor.js:160:13)",
    "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)",
    "    at async module.exports.handler (/var/task/executor.js:66:19)"
  ]
}
```

original func response:

```
{
    "StatusCode": 200,
    "FunctionError": "Unhandled",
    "ExecutedVersion": "$LATEST"
    "Payload": {"errorMessage":"2024-04-29T15:53:04.314Z 9fb99af1-d955-4c09-9e72-8e70756c327f Task timed out after 3.01 seconds"}
}
```


Patch:
diff --git a/lambda/executor.js b/lambda/executor.js
index 9f250ea..01f4e33 100644
--- a/lambda/executor.js
+++ b/lambda/executor.js
@@ -140,11 +140,8 @@ const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, post
         const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, lambdaAlias, payloads[i], preARN, postARN, disablePayloadLogs);
         // invocation errors return 200 and contain FunctionError and Payload
         if (invocationResults.FunctionError) {
-            let errorMessage = `Invocation error (running in parallel): ${invocationResults.Payload}`;
-            if (!disablePayloadLogs) {
-                errorMessage += ` with payload ${JSON.stringify(actualPayload)}`;
-            }
-            throw new Error(errorMessage);
+            let errorMessage = 'Invocation error (running in parallel)';
+            utils.handleLambdaInvocationError(errorMessage, invocationResults, actualPayload, disablePayloadLogs);
         }
         results.push(invocationResults);
     });
@@ -160,11 +157,8 @@ const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postAR
         const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, lambdaAlias, payloads[i], preARN, postARN, disablePayloadLogs);
         // invocation errors return 200 and contain FunctionError and Payload
         if (invocationResults.FunctionError) {
-            let errorMessage = `Invocation error (running in series): ${invocationResults.Payload}`;
-            if (!disablePayloadLogs) {
-                errorMessage += ` with payload ${JSON.stringify(actualPayload)}`;
-            }
-            throw new Error(errorMessage);
+            let errorMessage = 'Invocation error (running in series)';
+            utils.handleLambdaInvocationError(errorMessage, invocationResults, actualPayload, disablePayloadLogs);
         }
         if (sleepBetweenRunsMs > 0) {
             await utils.sleep(sleepBetweenRunsMs);
diff --git a/lambda/utils.js b/lambda/utils.js
index e83603c..d8cc142 100644
--- a/lambda/utils.js
+++ b/lambda/utils.js
@@ -254,11 +254,8 @@ module.exports.deleteLambdaAlias = (lambdaARN, alias) => {
 module.exports.invokeLambdaProcessor = async(processorARN, payload, preOrPost = 'Pre', disablePayloadLogs = false) => {
     const processorData = await utils.invokeLambda(processorARN, null, payload, disablePayloadLogs);
     if (processorData.FunctionError) {
-        let errorMessage = `${preOrPost}Processor ${processorARN} failed with error ${processorData.Payload}`;
-        if (!disablePayloadLogs) {
-            errorMessage += ` and payload ${JSON.stringify(payload)}`;
-        }
-        throw new Error(errorMessage);
+        let errorMessage = `${preOrPost}Processor ${processorARN} failed`;
+        utils.handleLambdaInvocationError(errorMessage, processorData, payload, disablePayloadLogs);
     }
     return processorData.Payload;
 };
@@ -315,6 +312,20 @@ module.exports.invokeLambda = (lambdaARN, alias, payload, disablePayloadLogs) =>
     return lambda.send(new InvokeCommand(params));
 };
 
+/**
+ * Handle a Lambda invocation error and generate an error message containing original error type, message and trace.
+ */
+module.exports.handleLambdaInvocationError = (errorMessageToDisplay, invocationResults, actualPayload, disablePayloadLogs) => {
+    const parsedResults = JSON.parse(Buffer.from(invocationResults.Payload));
+    if (!disablePayloadLogs) {
+        errorMessageToDisplay += ` with payload ${JSON.stringify(actualPayload)}`;
+    }
+    errorMessageToDisplay += ` - original error type: "${parsedResults.errorType}", ` +
+        `original error message: "${parsedResults.errorMessage}",` +
+        `trace: "${JSON.stringify(parsedResults.stackTrace)}"`;
+    throw new Error(errorMessageToDisplay);
+};
+
 /**
  * Fetch the body of an S3 object, given an S3 path such as s3://BUCKET/KEY
  */


Test patch:
diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js
index 96b758b..4b29359 100644
--- a/test/unit/test-lambda.js
+++ b/test/unit/test-lambda.js
@@ -65,6 +65,12 @@ const invokeForFailure = async(handler, event) => {
 
 };
 
+// utility to create a UInt8Array from a string
+const toByteArray = (inputString) => {
+    const textEncoder = new TextEncoder();
+    return textEncoder.encode(inputString);
+};
+
 // Stub stuff
 const sandBox = sinon.createSandbox();
 var getLambdaAliasStub,
@@ -775,7 +781,9 @@ describe('Lambda Functions', async() => {
                 .callsFake(async(_arn, _alias, payload) => {
                     return {
                         FunctionError: 'Unhandled',
-                        Payload: '{"errorType": "MemoryError", "stackTrace": [["/var/task/lambda_function.py", 11, "lambda_handler", "blabla"], ["/var/task/lambda_function.py", 7, "blabla]]}',
+                        Payload: toByteArray('{"errorMessage": "Exception raised during execution.", ' +
+                            '"errorType": "Exception", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ' +
+                            '"stackTrace": ["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]}'),
                     };
                 });
             await invokeForFailure(handler, {
@@ -796,7 +804,9 @@ describe('Lambda Functions', async() => {
                 .callsFake(async(_arn, _alias, payload) => {
                     return {
                         FunctionError: 'Unhandled',
-                        Payload: '{"errorType": "MemoryError", "stackTrace": [["/var/task/lambda_function.py", 11, "lambda_handler", "blabla"], ["/var/task/lambda_function.py", 7, "blabla]]}',
+                        Payload: toByteArray('{"errorMessage": "Exception raised during execution.", ' +
+                            '"errorType": "Exception", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ' +
+                            '"stackTrace": ["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]}'),
                     };
                 });
             const error = await invokeForFailure(handler, {
@@ -837,7 +847,9 @@ describe('Lambda Functions', async() => {
                 .callsFake(async(_arn, _alias, payload) => {
                     return {
                         FunctionError: 'Unhandled',
-                        Payload: '{"errorType": "MemoryError", "stackTrace": [["/var/task/lambda_function.py", 11, "lambda_handler", "blabla"], ["/var/task/lambda_function.py", 7, "blabla]]}',
+                        Payload: toByteArray('{"errorMessage": "Exception raised during execution.", ' +
+                            '"errorType": "Exception", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ' +
+                            '"stackTrace": ["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]}'),
                     };
                 });
             const error = await invokeForFailure(handler, {
@@ -879,7 +891,9 @@ describe('Lambda Functions', async() => {
                 .callsFake(async(_arn, _alias, payload) => {
                     return {
                         FunctionError: 'Unhandled',
-                        Payload: '{"errorType": "MemoryError", "stackTrace": [["/var/task/lambda_function.py", 11, "lambda_handler", "blabla"], ["/var/task/lambda_function.py", 7, "blabla]]}',
+                        Payload: toByteArray('{"errorMessage": "Exception raised during execution.", ' +
+                            '"errorType": "Exception", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ' +
+                            '"stackTrace": ["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]}'),
                     };
                 });
             const error = await invokeForFailure(handler, {
@@ -907,7 +921,9 @@ describe('Lambda Functions', async() => {
                 .callsFake(async(_arn, _alias, payload) => {
                     return {
                         FunctionError: 'Unhandled',
-                        Payload: '{"errorType": "MemoryError", "stackTrace": [["/var/task/lambda_function.py", 11, "lambda_handler", "blabla"], ["/var/task/lambda_function.py", 7, "blabla]]}',
+                        Payload: toByteArray('{"errorMessage": "Exception raised during execution.", ' +
+                            '"errorType": "Exception", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ' +
+                            '"stackTrace": ["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]}'),
                     };
                 });
             const error = await invokeForFailure(handler, {
@@ -936,7 +952,9 @@ describe('Lambda Functions', async() => {
                 .callsFake(async(_arn, _alias, payload) => {
                     return {
                         FunctionError: 'Unhandled',
-                        Payload: '{"errorType": "MemoryError", "stackTrace": [["/var/task/lambda_function.py", 11, "lambda_handler", "blabla"], ["/var/task/lambda_function.py", 7, "blabla]]}',
+                        Payload: toByteArray('{"errorMessage": "Exception raised during execution.", ' +
+                            '"errorType": "Exception", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ' +
+                            '"stackTrace": ["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]}'),
                     };
                 });
             await invokeForFailure(handler, {
@@ -1130,7 +1148,9 @@ describe('Lambda Functions', async() => {
                 .callsFake(async(_arn, _alias, payload) => {
                     return {
                         FunctionError: 'Unhandled',
-                        Payload: '{"errorType": "MemoryError", "stackTrace": [["/var/task/lambda_function.py", 11, "lambda_handler", "blabla"], ["/var/task/lambda_function.py", 7, "blabla]]}',
+                        Payload: toByteArray('{"errorMessage": "Exception raised during execution.", ' +
+                            '"errorType": "Exception", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ' +
+                            '"stackTrace": ["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]}'),
                     };
                 });
 
@@ -1166,7 +1186,9 @@ describe('Lambda Functions', async() => {
                 .callsFake(async(_arn, _alias, payload) => {
                     return {
                         FunctionError: 'Unhandled',
-                        Payload: '{"errorType": "MemoryError", "stackTrace": [["/var/task/lambda_function.py", 11, "lambda_handler", "blabla"], ["/var/task/lambda_function.py", 7, "blabla]]}',
+                        Payload: toByteArray('{"errorMessage": "Exception raised during execution.", ' +
+                            '"errorType": "Exception", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ' +
+                            '"stackTrace": ["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]}'),
                     };
                 });
 
diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js
index ae590e1..ff6f4d2 100644
--- a/test/unit/test-utils.js
+++ b/test/unit/test-utils.js
@@ -48,6 +48,12 @@ s3Mock.on(GetObjectCommand).resolves({
     }
 });
 
+// utility to create a UInt8Array from a string
+const toByteArray = (inputString) => {
+    const textEncoder = new TextEncoder();
+    return textEncoder.encode(inputString);
+};
+
 
 describe('Lambda Utils', () => {
 
@@ -531,7 +537,9 @@ describe('Lambda Utils', () => {
                 .callsFake(async () => {
                     invokeLambdaCounter++;
                     return {
-                        Payload: '{"KO": "KO"}',
+                        Payload: toByteArray('{"errorMessage": "Exception raised during execution.", ' +
+                            '"errorType": "Exception", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ' +
+                            '"stackTrace": ["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]}'),
                         FunctionError: 'Unhandled',
                     };
                 });
@@ -539,8 +547,8 @@ describe('Lambda Utils', () => {
                 const data = await utils.invokeLambdaProcessor('arnOK', payload, 'Pre', disablePayloadLogs);
                 expect(data).to.be(null);
             } catch (ex) {
-                expect(ex.message).to.contain('failed with error');
-                expect(ex.message.includes('and payload')).to.be(isPayloadInErrorMessage);
+                expect(ex.message).to.contain('failed');
+                expect(ex.message.includes('with payload')).to.be(isPayloadInErrorMessage);
             }
 
             expect(invokeLambdaCounter).to.be(1);
@@ -569,6 +577,48 @@ describe('Lambda Utils', () => {
         return true;
     };
 
+    describe('handleLambdaInvocationError', () => {
+
+        const invokeLambdaForInvocationErrorAndAssertOnErrorMessage = async({disablePayloadLogs, isPayloadInErrorMessage}) => {
+            const errorMessage = 'Encountered invocation error';
+            const originalErrorMessage = 'Exception raised during execution.';
+            const originalErrorType = 'Exception';
+            const originalStackTrace = '["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]';
+            const invocationResults = {
+                Payload: toByteArray(`{"errorMessage": "${originalErrorMessage}", ` +
+                    `"errorType": "${originalErrorType}", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ` +
+                    `"stackTrace": ${originalStackTrace}}`),
+                FunctionError: 'Unhandled',
+            };
+            const actualPayload = 'TEST_PAYLOAD';
+
+            try {
+                utils.handleLambdaInvocationError(errorMessage, invocationResults, actualPayload, disablePayloadLogs);
+            } catch (error) {
+                expect(error.message).to.contain(errorMessage);
+                expect(error.message).to.contain(originalErrorMessage);
+                expect(error.message).to.contain(originalErrorType);
+                expect(error.message).to.contain(originalStackTrace);
+                expect(error.message.includes(actualPayload)).to.be(isPayloadInErrorMessage);
+            }
+        };
+
+        it('should NOT contain not payload in error message if display payload logging is disabled', async() => invokeLambdaForInvocationErrorAndAssertOnErrorMessage({
+            disablePayloadLogs: true,
+            isPayloadInErrorMessage: false,
+        }));
+
+        it('should contain payload in error message if display payload logging is NOT disabled', async() => invokeLambdaForInvocationErrorAndAssertOnErrorMessage({
+            disablePayloadLogs: false,
+            isPayloadInErrorMessage: true,
+        }));
+
+        it('should contain payload in error message if disablePayloadLogs is undefined', async() => invokeLambdaForInvocationErrorAndAssertOnErrorMessage({
+            disablePayloadLogs: undefined,
+            isPayloadInErrorMessage: true,
+        }));
+    });
+
     describe('convertPayload', () => {
 
         it('should JSON-encode strings, if not JSON strings already', async () => {