Skip to content

Commit 1ab9990

Browse files
author
Fabiana Severin
committed
add async handler detection tests
1 parent 2a37745 commit 1ab9990

File tree

7 files changed

+118
-15
lines changed

7 files changed

+118
-15
lines changed

src/Errors.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ function toRapidResponse(error) {
3838
try {
3939
if (util.types.isNativeError(error) || _isError(error)) {
4040
return {
41-
errorType: error.name?.replace(/\x7F/g, '%7F'),
42-
errorMessage: error.message?.replace(/\x7F/g, '%7F'),
43-
trace: error.stack.replace(/\x7F/g, '%7F').split('\n'),
41+
errorType: error.name?.replaceAll('\x7F', '%7F'),
42+
errorMessage: error.message?.replaceAll('\x7F', '%7F'),
43+
trace: error.stack.replaceAll('\x7F', '%7F').split('\n'),
4444
};
4545
} else {
4646
return {
@@ -106,6 +106,13 @@ const errorClasses = [
106106
class UserCodeSyntaxError extends Error {},
107107
class MalformedStreamingHandler extends Error {},
108108
class InvalidStreamingOperation extends Error {},
109+
class NodeJsExit extends Error {
110+
constructor() {
111+
super(
112+
'The Lambda runtime client detected an unexpected Node.js exit code. This is most commonly caused by a Promise that was never settled. For more information, see https://nodejs.org/docs/latest/api/process.html#exit-codes',
113+
);
114+
}
115+
},
109116
class UnhandledPromiseRejection extends Error {
110117
constructor(reason, promise) {
111118
super(reason);

src/Runtime.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ const CallbackContext = require('./CallbackContext.js');
1212
const StreamingContext = require('./StreamingContext.js');
1313
const BeforeExitListener = require('./BeforeExitListener.js');
1414
const { STREAM_RESPONSE } = require('./UserFunction.js');
15+
const { NodeJsExit } = require('./Errors.js');
1516
const { verbose, vverbose } = require('./VerboseLog.js').logger('RAPID');
17+
const { structuredConsole } = require('./LogPatch');
1618

1719
module.exports = class Runtime {
1820
constructor(client, handler, handlerMetadata, errorCallbacks) {
@@ -69,7 +71,7 @@ module.exports = class Runtime {
6971

7072
try {
7173
this._setErrorCallbacks(invokeContext.invokeId);
72-
this._setDefaultExitListener(invokeContext.invokeId, markCompleted);
74+
this._setDefaultExitListener(invokeContext.invokeId, markCompleted, this.handlerMetadata.isAsync);
7375

7476
let result = this.handler(
7577
JSON.parse(bodyJson),
@@ -178,12 +180,22 @@ module.exports = class Runtime {
178180
* called and the handler is not async.
179181
* CallbackContext replaces the listener if a callback is invoked.
180182
*/
181-
_setDefaultExitListener(invokeId, markCompleted) {
183+
_setDefaultExitListener(invokeId, markCompleted, isAsync) {
182184
BeforeExitListener.set(() => {
183185
markCompleted();
184-
this.client.postInvocationResponse(null, invokeId, () =>
185-
this.scheduleIteration(),
186-
);
186+
// if the handle signature is async, we do want to fail the invocation
187+
if (isAsync) {
188+
const nodeJsExitError = new NodeJsExit();
189+
structuredConsole.logError('Invoke Error', nodeJsExitError);
190+
this.client.postInvocationError(nodeJsExitError, invokeId, () =>
191+
this.scheduleIteration(),
192+
);
193+
// if the handler signature is sync, or use callback, we do want to send a successful invocation with a null payload if the customer forgot to call the callback
194+
} else {
195+
this.client.postInvocationResponse(null, invokeId, () =>
196+
this.scheduleIteration(),
197+
);
198+
}
187199
});
188200
}
189201

src/UserFunction.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,17 @@ module.exports.isHandlerFunction = function (value) {
311311
return typeof value === 'function';
312312
};
313313

314-
function _isAsync(handlerFunc) {
315-
return handlerFunc.constructor.name === 'AsyncFunction';
314+
function _isAsync(handler) {
315+
try {
316+
return (
317+
handler &&
318+
typeof handler === 'function' &&
319+
handler.constructor &&
320+
handler.constructor.name === 'AsyncFunction'
321+
);
322+
} catch (error) {
323+
return false;
324+
}
316325
}
317326

318327
module.exports.getHandlerMetadata = function (handlerFunc) {

test/handlers/isAsync.mjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/** Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
2+
3+
export const handlerAsync = async () => {
4+
const response = {
5+
statusCode: 200,
6+
body: JSON.stringify('Hello from Lambda!'),
7+
};
8+
return response;
9+
};
10+
11+
export const handlerNotAsync = () => {
12+
const response = {
13+
statusCode: 200,
14+
body: JSON.stringify('Hello from Lambda!'),
15+
};
16+
return response;
17+
};

test/handlers/isAsyncCallback.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/** Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
2+
3+
'use strict';
4+
5+
exports.handler = (_event, _context, callback) => {
6+
callback(null, {
7+
statusCode: 200,
8+
body: JSON.stringify({
9+
message: 'hello world',
10+
}),
11+
});
12+
};

test/unit/ErrorsTest.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
/**
2-
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3-
*/
1+
/** Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
42

53
'use strict';
64

@@ -22,11 +20,22 @@ describe('Formatted Error Logging', () => {
2220

2321
describe('Invalid chars in HTTP header', () => {
2422
it('should be replaced', () => {
25-
let errorWithInvalidChar = new Error('\x7F \x7F');
23+
let errorWithInvalidChar = new Error('\x7F');
2624
errorWithInvalidChar.name = 'ErrorWithInvalidChar';
2725

2826
let loggedError = Errors.toRapidResponse(errorWithInvalidChar);
2927
loggedError.should.have.property('errorType', 'ErrorWithInvalidChar');
30-
loggedError.should.have.property('errorMessage', '%7F %7F');
28+
loggedError.should.have.property('errorMessage', '%7F');
29+
});
30+
});
31+
32+
describe('NodeJsExit error ctor', () => {
33+
it('should be have a fixed reason', () => {
34+
let nodeJsExit = new Errors.NodeJsExit();
35+
let loggedError = Errors.toRapidResponse(nodeJsExit);
36+
loggedError.should.have.property('errorType', 'Runtime.NodeJsExit');
37+
loggedError.errorMessage.should.containEql(
38+
'runtime client detected an unexpected Node.js',
39+
);
3140
});
3241
});

test/unit/IsAsyncTest.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/** Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
2+
3+
'use strict';
4+
5+
require('should');
6+
const path = require('path');
7+
const UserFunction = require('lambda-runtime/UserFunction.js');
8+
9+
const TEST_ROOT = path.join(__dirname, '../');
10+
const HANDLERS_ROOT = path.join(TEST_ROOT, 'handlers');
11+
12+
describe('isAsync tests', () => {
13+
it('is async should be true', async () => {
14+
const handlerFunc = await UserFunction.load(
15+
HANDLERS_ROOT,
16+
'isAsync.handlerAsync',
17+
);
18+
const metadata = UserFunction.getHandlerMetadata(handlerFunc);
19+
metadata.isAsync.should.be.true();
20+
});
21+
it('is async should be false', async () => {
22+
const handlerFunc = await UserFunction.load(
23+
HANDLERS_ROOT,
24+
'isAsync.handlerNotAsync',
25+
);
26+
const metadata = UserFunction.getHandlerMetadata(handlerFunc);
27+
metadata.isAsync.should.be.false();
28+
});
29+
it('is async should be false since it is a callback', async () => {
30+
const handlerFunc = await UserFunction.load(
31+
HANDLERS_ROOT,
32+
'isAsyncCallback.handler',
33+
);
34+
const metadata = UserFunction.getHandlerMetadata(handlerFunc);
35+
metadata.isAsync.should.be.false();
36+
});
37+
});

0 commit comments

Comments
 (0)