Skip to content

Adding warning callback handlers and isAsync detection #144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions .github/workflows/test-on-push-and-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,4 @@ jobs:
docker build -f test/unit/Dockerfile.nodejs${{ matrix.node-version }}.x -t unit/nodejs.${{ matrix.node-version }}x .
docker run unit/nodejs.${{ matrix.node-version }}x

integration-test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
distro: [alpine, amazonlinux, centos, debian, ubuntu]

steps:
- uses: actions/checkout@v4
- name: Run ${{ matrix.distro }} integration tests
run: DISTRO=${{ matrix.distro }} make test-integ
13 changes: 2 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,6 @@ init:
test:
npm run test

setup-codebuild-agent:
docker build -t codebuild-agent - < test/integration/codebuild-local/Dockerfile.agent

test-smoke: setup-codebuild-agent
CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.alpine.1.yml alpine 3.16 18

test-integ: setup-codebuild-agent
CODEBUILD_IMAGE_TAG=codebuild-agent DISTRO="$(DISTRO)" test/integration/codebuild-local/test_all.sh test/integration/codebuild

copy-files:
npm run copy-files

Expand All @@ -30,7 +21,7 @@ format:
dev: init test

# Verifications to run before sending a pull request
pr: build dev test-smoke
pr: build dev

clean:
npm run clean
Expand All @@ -42,7 +33,7 @@ build: copy-files
pack: build
npm pack

.PHONY: target init test setup-codebuild-agent test-smoke test-integ install format dev pr clean build pack copy-files
.PHONY: target init test install format dev pr clean build pack copy-files

define HELP_MESSAGE

Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,6 @@ make init build
```
Then,
* to run unit tests: `make test`
* to run integration tests: `make test-integ`
* to run smoke tests: `make test-smoke`

### Raising a PR
When modifying dependencies (`package.json`), make sure to:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

13 changes: 10 additions & 3 deletions src/Errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ function toRapidResponse(error) {
try {
if (util.types.isNativeError(error) || _isError(error)) {
return {
errorType: error.name?.replace(/\x7F/g, '%7F'),
errorMessage: error.message?.replace(/\x7F/g, '%7F'),
trace: error.stack.replace(/\x7F/g, '%7F').split('\n'),
errorType: error.name?.replaceAll('\x7F', '%7F'),
errorMessage: error.message?.replaceAll('\x7F', '%7F'),
trace: error.stack.replaceAll('\x7F', '%7F').split('\n'),
};
} else {
return {
Expand Down Expand Up @@ -106,6 +106,13 @@ const errorClasses = [
class UserCodeSyntaxError extends Error {},
class MalformedStreamingHandler extends Error {},
class InvalidStreamingOperation extends Error {},
class NodeJsExit extends Error {
constructor() {
super(
'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',
);
}
},
class UnhandledPromiseRejection extends Error {
constructor(reason, promise) {
super(reason);
Expand Down
22 changes: 17 additions & 5 deletions src/Runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const CallbackContext = require('./CallbackContext.js');
const StreamingContext = require('./StreamingContext.js');
const BeforeExitListener = require('./BeforeExitListener.js');
const { STREAM_RESPONSE } = require('./UserFunction.js');
const { NodeJsExit } = require('./Errors.js');
const { verbose, vverbose } = require('./VerboseLog.js').logger('RAPID');
const { structuredConsole } = require('./LogPatch');

module.exports = class Runtime {
constructor(client, handler, handlerMetadata, errorCallbacks) {
Expand Down Expand Up @@ -69,7 +71,7 @@ module.exports = class Runtime {

try {
this._setErrorCallbacks(invokeContext.invokeId);
this._setDefaultExitListener(invokeContext.invokeId, markCompleted);
this._setDefaultExitListener(invokeContext.invokeId, markCompleted, this.handlerMetadata.isAsync);

let result = this.handler(
JSON.parse(bodyJson),
Expand Down Expand Up @@ -178,12 +180,22 @@ module.exports = class Runtime {
* called and the handler is not async.
* CallbackContext replaces the listener if a callback is invoked.
*/
_setDefaultExitListener(invokeId, markCompleted) {
_setDefaultExitListener(invokeId, markCompleted, isAsync) {
BeforeExitListener.set(() => {
markCompleted();
this.client.postInvocationResponse(null, invokeId, () =>
this.scheduleIteration(),
);
// if the handle signature is async, we do want to fail the invocation
if (isAsync) {
const nodeJsExitError = new NodeJsExit();
structuredConsole.logError('Invoke Error', nodeJsExitError);
this.client.postInvocationError(nodeJsExitError, invokeId, () =>
this.scheduleIteration(),
);
// 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
} else {
this.client.postInvocationResponse(null, invokeId, () =>
this.scheduleIteration(),
);
}
});
}

Expand Down
15 changes: 15 additions & 0 deletions src/UserFunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,25 @@ module.exports.isHandlerFunction = function (value) {
return typeof value === 'function';
};

function _isAsync(handler) {
try {
return (
handler &&
typeof handler === 'function' &&
handler.constructor &&
handler.constructor.name === 'AsyncFunction'
);
} catch (error) {
return false;
}
}

module.exports.getHandlerMetadata = function (handlerFunc) {
return {
streaming: _isHandlerStreaming(handlerFunc),
highWaterMark: _highWaterMark(handlerFunc),
isAsync: _isAsync(handlerFunc),
argsNum: handlerFunc.length,
};
};

Expand Down
24 changes: 24 additions & 0 deletions src/WarningForCallbackHandlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

'use strict';

const shouldWarnOnCallbackFunctionUse = (metadata) => {
return (
process.env.AWS_LAMBDA_NODEJS_DISABLE_CALLBACK_WARNING === undefined &&
metadata !== undefined &&
metadata.argsNum == 3 &&
metadata.isAsync == false &&
metadata.streaming == false
);
};

module.exports.checkForDeprecatedCallback = function (metadata) {
if (shouldWarnOnCallbackFunctionUse(metadata)) {
console.warn(
`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.`,
);
}
};
2 changes: 2 additions & 0 deletions src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const UserFunction = require('./UserFunction.js');
const Errors = require('./Errors.js');
const BeforeExitListener = require('./BeforeExitListener.js');
const LogPatch = require('./LogPatch');
const { checkForDeprecatedCallback } = require('./WarningForCallbackHandlers');

export async function run(appRootOrHandler, handler = '') {
LogPatch.patchConsole();
Expand Down Expand Up @@ -44,6 +45,7 @@ export async function run(appRootOrHandler, handler = '') {
: await UserFunction.load(appRootOrHandler, handler);

const metadata = UserFunction.getHandlerMetadata(handlerFunc);
checkForDeprecatedCallback(metadata);
new Runtime(
client,
handlerFunc,
Expand Down
17 changes: 17 additions & 0 deletions test/handlers/isAsync.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. */

export const handlerAsync = async () => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};

export const handlerNotAsync = () => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
12 changes: 12 additions & 0 deletions test/handlers/isAsyncCallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/** Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. */

'use strict';

exports.handler = (_event, _context, callback) => {
callback(null, {
statusCode: 200,
body: JSON.stringify({
message: 'hello world',
}),
});
};
5 changes: 0 additions & 5 deletions test/integration/codebuild-local/Dockerfile.agent

This file was deleted.

Loading