Skip to content

Commit a763632

Browse files
authored
Merge pull request #153 from aws/main
merge main
2 parents 595e7e8 + da5777a commit a763632

13 files changed

+222
-37
lines changed

.github/workflows/test-on-push-and-pr.yml

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,10 @@ jobs:
1515
node-version: [18, 20, 22]
1616

1717
steps:
18-
- uses: actions/checkout@v4
18+
- uses: actions/checkout@v5
1919
- name: Build and run tests for Node.js ${{ matrix.node-version }}
2020
run: |
2121
docker build -f test/unit/Dockerfile.nodejs${{ matrix.node-version }}.x -t unit/nodejs.${{ matrix.node-version }}x .
2222
docker run unit/nodejs.${{ matrix.node-version }}x
2323
24-
integration-test:
25-
runs-on: ubuntu-latest
26-
strategy:
27-
fail-fast: false
28-
matrix:
29-
distro: [alpine, amazonlinux, centos, debian, ubuntu]
3024
31-
steps:
32-
- uses: actions/checkout@v4
33-
- name: Run ${{ matrix.distro }} integration tests
34-
run: DISTRO=${{ matrix.distro }} make test-integ

Makefile

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,6 @@ init:
88
test:
99
npm run test
1010

11-
setup-codebuild-agent:
12-
docker build -t codebuild-agent - < test/integration/codebuild-local/Dockerfile.agent
13-
14-
test-smoke: setup-codebuild-agent
15-
CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.alpine.1.yml alpine 3.16 18
16-
17-
test-integ: setup-codebuild-agent
18-
CODEBUILD_IMAGE_TAG=codebuild-agent DISTRO="$(DISTRO)" test/integration/codebuild-local/test_all.sh test/integration/codebuild
19-
2011
copy-files:
2112
npm run copy-files
2213

@@ -30,7 +21,7 @@ format:
3021
dev: init test
3122

3223
# Verifications to run before sending a pull request
33-
pr: build dev test-smoke
24+
pr: build dev
3425

3526
clean:
3627
npm run clean
@@ -42,7 +33,7 @@ build: copy-files
4233
pack: build
4334
npm pack
4435

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

4738
define HELP_MESSAGE
4839

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,6 @@ make init build
162162
```
163163
Then,
164164
* to run unit tests: `make test`
165-
* to run integration tests: `make test-integ`
166-
* to run smoke tests: `make test-smoke`
167165

168166
### Raising a PR
169167
When modifying dependencies (`package.json`), make sure to:

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

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+
};

0 commit comments

Comments
 (0)