Skip to content

Commit b4e31e1

Browse files
authored
Merge pull request #144 from aws/fabisev/warning-callback-handlers
Adding warning callback handlers and isAsync detection
2 parents 2f679f2 + cbfa8ba commit b4e31e1

File tree

10 files changed

+216
-13
lines changed

10 files changed

+216
-13
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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,25 @@ module.exports.isHandlerFunction = function (value) {
311311
return typeof value === 'function';
312312
};
313313

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+
}
325+
}
326+
314327
module.exports.getHandlerMetadata = function (handlerFunc) {
315328
return {
316329
streaming: _isHandlerStreaming(handlerFunc),
317330
highWaterMark: _highWaterMark(handlerFunc),
331+
isAsync: _isAsync(handlerFunc),
332+
argsNum: handlerFunc.length,
318333
};
319334
};
320335

src/WarningForCallbackHandlers.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict';
7+
8+
const shouldWarnOnCallbackFunctionUse = (metadata) => {
9+
return (
10+
process.env.AWS_LAMBDA_NODEJS_DISABLE_CALLBACK_WARNING === undefined &&
11+
metadata !== undefined &&
12+
metadata.argsNum == 3 &&
13+
metadata.isAsync == false &&
14+
metadata.streaming == false
15+
);
16+
};
17+
18+
module.exports.checkForDeprecatedCallback = function (metadata) {
19+
if (shouldWarnOnCallbackFunctionUse(metadata)) {
20+
console.warn(
21+
`AWS Lambda plans to remove support for callback-based function handlers starting with Node.js 24. You will need to update this function to use an async handler to use Node.js 24 or later. For more information and to provide feedback on this change, see https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/issues/137. To disable this warning, set the AWS_LAMBDA_NODEJS_DISABLE_CALLBACK_WARNING environment variable.`,
22+
);
23+
}
24+
};

src/index.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const UserFunction = require('./UserFunction.js');
1111
const Errors = require('./Errors.js');
1212
const BeforeExitListener = require('./BeforeExitListener.js');
1313
const LogPatch = require('./LogPatch');
14+
const { checkForDeprecatedCallback } = require('./WarningForCallbackHandlers');
1415

1516
export async function run(appRootOrHandler, handler = '') {
1617
LogPatch.patchConsole();
@@ -44,6 +45,7 @@ export async function run(appRootOrHandler, handler = '') {
4445
: await UserFunction.load(appRootOrHandler, handler);
4546

4647
const metadata = UserFunction.getHandlerMetadata(handlerFunc);
48+
checkForDeprecatedCallback(metadata);
4749
new Runtime(
4850
client,
4951
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+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict';
7+
8+
require('should');
9+
10+
let { captureStream, consoleSnapshot } = require('./LoggingGlobals');
11+
12+
let {
13+
checkForDeprecatedCallback,
14+
} = require('../../src/WarningForCallbackHandlers.js');
15+
16+
let LogPatch = require('lambda-runtime/LogPatch');
17+
const UserFunction = require('lambda-runtime/UserFunction.js');
18+
19+
const path = require('path');
20+
const TEST_ROOT = path.join(__dirname, '../');
21+
const HANDLERS_ROOT = path.join(TEST_ROOT, 'handlers');
22+
23+
describe('Formatted Error Logging', () => {
24+
let restoreConsole = consoleSnapshot();
25+
let capturedStdout = captureStream(process.stdout);
26+
27+
beforeEach(
28+
'delete env var',
29+
() => delete process.env.AWS_LAMBDA_NODEJS_DISABLE_CALLBACK_WARNING,
30+
);
31+
beforeEach('capture stdout', () => capturedStdout.hook());
32+
beforeEach('apply console patch', () => LogPatch.patchConsole());
33+
afterEach('remove console patch', () => restoreConsole());
34+
afterEach('unhook stdout', () => capturedStdout.unhook());
35+
36+
const expectedString =
37+
'AWS Lambda plans to remove support for callback-based function handlers';
38+
39+
const tests = [
40+
{ args: [false, 'isAsyncCallback.handler'], expected: true },
41+
{ args: [true, 'isAsyncCallback.handler'], expected: false },
42+
{ args: [false, 'isAsync.handlerAsync'], expected: false },
43+
{ args: [true, 'isAsync.handlerAsync'], expected: false },
44+
{ args: [false, 'defaultHandler.default'], expected: false },
45+
{ args: [true, 'defaultHandler.default'], expected: false },
46+
];
47+
48+
tests.forEach(({ args, expected }) => {
49+
const shouldDeclareEnv = args[0];
50+
const handler = args[1];
51+
it(`When AWS_LAMBDA_NODEJS_DISABLE_CALLBACK_WARNING=${shouldDeclareEnv} expecting ${
52+
expected ? 'no ' : ''
53+
}warning logs for handler ${handler}`, async () => {
54+
if (shouldDeclareEnv) {
55+
process.env.AWS_LAMBDA_NODEJS_DISABLE_CALLBACK_WARNING = 1;
56+
}
57+
const handlerFunc = await UserFunction.load(HANDLERS_ROOT, handler);
58+
const metadata = UserFunction.getHandlerMetadata(handlerFunc);
59+
60+
checkForDeprecatedCallback(metadata);
61+
if (expected) {
62+
capturedStdout.captured().should.containEql(expectedString);
63+
} else {
64+
capturedStdout.captured().should.not.containEql(expectedString);
65+
}
66+
});
67+
});
68+
});

0 commit comments

Comments
 (0)