From 976f76832d890036758ab5142cceb1975ae37dbe Mon Sep 17 00:00:00 2001 From: tsushanth <78000697+tsushanth@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:13:04 -0700 Subject: [PATCH] fix(receivers): make AwsLambdaReceiver.toHandler() promise-based (Node.js 24+) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #2761. `AwsLambdaReceiver.toHandler()` returned a 3-arg function `(event, context, callback) => Promise`. AWS Lambda's Node.js 24+ runtime checks the handler's `.length` up front and rejects 3-arg handlers with `Runtime.CallbackHandlerDeprecated` before invoking it — every Bolt app deployed on the Node 24 runtime crashed at startup. The callback parameter was never actually invoked anywhere in the receiver (the handler always returned a `Promise` directly), so drop it from both the `AwsHandler` type and the implementation. The existing `AwsCallback` export is preserved for source compatibility and annotated `@deprecated` so consumers see the migration path on hover. Existing tests are updated to call the handler with two arguments; a regression test pins `handler.length === 2` so a future refactor that re-adds a trailing parameter doesn't silently break Node 24 again. All 488 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/receivers/AwsLambdaReceiver.ts | 17 +++++++- test/unit/receivers/AwsLambdaReceiver.spec.ts | 43 ++++++++++++------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/receivers/AwsLambdaReceiver.ts b/src/receivers/AwsLambdaReceiver.ts index d1f43f6d0..7ade78f57 100644 --- a/src/receivers/AwsLambdaReceiver.ts +++ b/src/receivers/AwsLambdaReceiver.ts @@ -46,6 +46,15 @@ export interface AwsEventV2 { version: string; } +/** + * Legacy callback signature retained for backwards compatibility with consumer + * code. `AwsHandler` is now a 2-arg promise-based signature; this callback is + * no longer invoked by `AwsLambdaReceiver`. + * + * @deprecated Lambda's Node.js 24+ runtimes reject callback-style handlers + * (`Runtime.CallbackHandlerDeprecated`). Return a `Promise` + * directly instead. + */ // biome-ignore lint/suspicious/noExplicitAny: userland function results can be anything export type AwsCallback = (error?: Error | string | null, result?: any) => void; @@ -70,7 +79,7 @@ export interface AwsResponse { } // biome-ignore lint/suspicious/noExplicitAny: request context can be anything -export type AwsHandler = (event: AwsEvent, context: any, callback: AwsCallback) => Promise; +export type AwsHandler = (event: AwsEvent, context: any) => Promise; export interface AwsLambdaReceiverOptions { /** @@ -191,8 +200,12 @@ export default class AwsLambdaReceiver implements Receiver { } public toHandler(): AwsHandler { + // Returns a 2-arg promise-based handler so the resulting function has + // `length === 2`. Lambda's Node.js 24+ runtimes reject 3-arg handlers + // up front with `Runtime.CallbackHandlerDeprecated` before the handler + // is invoked. // biome-ignore lint/suspicious/noExplicitAny: request context can be anything - return async (awsEvent: AwsEvent, _awsContext: any, _awsCallback: AwsCallback): Promise => { + return async (awsEvent: AwsEvent, _awsContext: any): Promise => { this.logger.debug(`AWS event: ${JSON.stringify(awsEvent, null, 2)}`); const rawBody = this.getRawBody(awsEvent); diff --git a/test/unit/receivers/AwsLambdaReceiver.spec.ts b/test/unit/receivers/AwsLambdaReceiver.spec.ts index c913e81f8..a3d6eb800 100644 --- a/test/unit/receivers/AwsLambdaReceiver.spec.ts +++ b/test/unit/receivers/AwsLambdaReceiver.spec.ts @@ -66,6 +66,19 @@ describe('AwsLambdaReceiver', () => { await awsReceiver.stop(); }); + // Regression for slackapi/bolt-js#2761: Lambda's Node.js 24+ runtime rejects + // handlers whose `.length` is > 2 with `Runtime.CallbackHandlerDeprecated` + // before the handler is ever invoked. `toHandler()` must return a 2-arg + // promise-based function. + it('should return a 2-arg promise-based handler (Node.js 24+ Lambda runtime)', () => { + const awsReceiver = new AwsLambdaReceiver({ + signingSecret: 'my-secret', + logger: noopLogger, + }); + const handler = awsReceiver.toHandler(); + assert.equal(handler.length, 2); + }); + it('should return a 404 if app has no registered handlers for an incoming event, and return a 200 if app does have registered handlers', async () => { const awsReceiver = new AwsLambdaReceiver({ signingSecret: 'my-secret', @@ -76,7 +89,7 @@ describe('AwsLambdaReceiver', () => { const args = createDummyAppMentionEventMiddlewareArgs(); const body = JSON.stringify(args.body); const awsEvent = createDummyAWSPayload(body, timestamp); - const response1 = await handler(awsEvent, {}, (_error, _result) => {}); + const response1 = await handler(awsEvent, {}); assert.equal(response1.statusCode, 404); const App = importApp(appOverrides); const app = new App({ @@ -84,7 +97,7 @@ describe('AwsLambdaReceiver', () => { receiver: awsReceiver, }); app.event('app_mention', noopVoid); - const response2 = await handler(awsEvent, {}, (_error, _result) => {}); + const response2 = await handler(awsEvent, {}); assert.equal(response2.statusCode, 200); }); @@ -106,7 +119,7 @@ describe('AwsLambdaReceiver', () => { 'x-slack-request-timestamp': `${timestamp}`, 'x-slack-signature': `v0=${signature}`, }); - const response1 = await handler(awsEvent, {}, (_error, _result) => {}); + const response1 = await handler(awsEvent, {}); assert.equal(response1.statusCode, 404); const App = importApp(appOverrides); const app = new App({ @@ -114,7 +127,7 @@ describe('AwsLambdaReceiver', () => { receiver: awsReceiver, }); app.event('app_mention', noopVoid); - const response2 = await handler(awsEvent, {}, (_error, _result) => {}); + const response2 = await handler(awsEvent, {}); assert.equal(response2.statusCode, 200); }); @@ -136,7 +149,7 @@ describe('AwsLambdaReceiver', () => { 'X-Slack-Request-Timestamp': `${timestamp}`, 'X-Slack-Signature': `v0=${signature}`, }); - const response1 = await handler(awsEvent, {}, (_error, _result) => {}); + const response1 = await handler(awsEvent, {}); assert.equal(response1.statusCode, 404); const App = importApp(appOverrides); const app = new App({ @@ -146,7 +159,7 @@ describe('AwsLambdaReceiver', () => { app.shortcut('bolt-js-aws-lambda-shortcut', async ({ ack }) => { await ack(); }); - const response2 = await handler(awsEvent, {}, (_error, _result) => {}); + const response2 = await handler(awsEvent, {}); assert.equal(response2.statusCode, 200); }); @@ -168,7 +181,7 @@ describe('AwsLambdaReceiver', () => { 'X-Slack-Request-Timestamp': `${timestamp}`, 'X-Slack-Signature': `v0=${signature}`, }); - const response1 = await handler(awsEvent, {}, (_error, _result) => {}); + const response1 = await handler(awsEvent, {}); assert.equal(response1.statusCode, 404); const App = importApp(appOverrides); const app = new App({ @@ -178,7 +191,7 @@ describe('AwsLambdaReceiver', () => { app.command('/hello-bolt-js', async ({ ack }) => { await ack(); }); - const response2 = await handler(awsEvent, {}, (_error, _result) => {}); + const response2 = await handler(awsEvent, {}); assert.equal(response2.statusCode, 200); }); @@ -192,7 +205,7 @@ describe('AwsLambdaReceiver', () => { const args = createDummyAppMentionEventMiddlewareArgs(); const body = JSON.stringify(args.body); const awsEvent = createDummyAWSPayload(body, timestamp, undefined, true); - const response1 = await handler(awsEvent, {}, (_error, _result) => {}); + const response1 = await handler(awsEvent, {}); assert.equal(response1.statusCode, 404); }); @@ -213,7 +226,7 @@ describe('AwsLambdaReceiver', () => { 'X-Slack-Request-Timestamp': `${timestamp}`, 'X-Slack-Signature': `v0=${signature}`, }); - const response = await handler(awsEvent, {}, (_error, _result) => {}); + const response = await handler(awsEvent, {}); assert.equal(response.statusCode, 200); }); @@ -231,7 +244,7 @@ describe('AwsLambdaReceiver', () => { }); const handler = awsReceiver.toHandler(); const awsEvent = createDummyAWSPayload(urlVerificationBody, timestamp); - const response = await handler(awsEvent, {}, (_error, _result) => {}); + const response = await handler(awsEvent, {}); assert.equal(response.statusCode, 200); }); @@ -269,7 +282,7 @@ describe('AwsLambdaReceiver', () => { body: urlVerificationBody, isBase64Encoded: false, }; - const response = await handler(awsEvent, {}, (_error, _result) => {}); + const response = await handler(awsEvent, {}); assert.equal(response.statusCode, 401); assert(spy.calledOnce); }); @@ -282,7 +295,7 @@ describe('AwsLambdaReceiver', () => { const handler = awsReceiver.toHandler(); const timestamp = Math.floor(Date.now() / 1000) - 600; // 10 minutes ago const awsEvent = createDummyAWSPayload(urlVerificationBody, timestamp); - const response = await handler(awsEvent, {}, (_error, _result) => {}); + const response = await handler(awsEvent, {}); assert.equal(response.statusCode, 401); }); @@ -294,7 +307,7 @@ describe('AwsLambdaReceiver', () => { }); const handler = awsReceiver.toHandler(); const awsEvent = createDummyAWSPayload(urlVerificationBody); - const response = await handler(awsEvent, {}, (_error, _result) => {}); + const response = await handler(awsEvent, {}); assert.equal(response.statusCode, 200); }); @@ -311,7 +324,7 @@ describe('AwsLambdaReceiver', () => { const args = createDummyAppMentionEventMiddlewareArgs(); const body = JSON.stringify(args.body); const awsEvent = createDummyAWSPayload(body, timestamp); - const response1 = await handler(awsEvent, {}, (_error, _result) => {}); + const response1 = await handler(awsEvent, {}); assert.equal(response1.statusCode, 404); await new Promise((res) => { setTimeout(res, delay + 2);