Skip to content
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
17 changes: 15 additions & 2 deletions src/receivers/AwsLambdaReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AwsResponse>`
* directly instead.
*/
// biome-ignore lint/suspicious/noExplicitAny: userland function results can be anything
export type AwsCallback = (error?: Error | string | null, result?: any) => void;

Expand All @@ -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<AwsResponse>;
export type AwsHandler = (event: AwsEvent, context: any) => Promise<AwsResponse>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Good call dropping the callback from the exported type rather than just the implementation.


export interface AwsLambdaReceiverOptions {
/**
Expand Down Expand Up @@ -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<AwsResponse> => {
return async (awsEvent: AwsEvent, _awsContext: any): Promise<AwsResponse> => {
this.logger.debug(`AWS event: ${JSON.stringify(awsEvent, null, 2)}`);

const rawBody = this.getRawBody(awsEvent);
Expand Down
43 changes: 28 additions & 15 deletions test/unit/receivers/AwsLambdaReceiver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Thanks for assert.equal(handler.length, 2) to pin the handler, since that is the exact value Lambda runtime reads.

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',
Expand All @@ -76,15 +89,15 @@ 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({
token: 'xoxb-',
receiver: awsReceiver,
});
app.event('app_mention', noopVoid);
const response2 = await handler(awsEvent, {}, (_error, _result) => {});
const response2 = await handler(awsEvent, {});
assert.equal(response2.statusCode, 200);
});

Expand All @@ -106,15 +119,15 @@ 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({
token: 'xoxb-',
receiver: awsReceiver,
});
app.event('app_mention', noopVoid);
const response2 = await handler(awsEvent, {}, (_error, _result) => {});
const response2 = await handler(awsEvent, {});
assert.equal(response2.statusCode, 200);
});

Expand All @@ -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({
Expand All @@ -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);
});

Expand All @@ -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({
Expand All @@ -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);
});

Expand All @@ -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);
});

Expand All @@ -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);
});

Expand All @@ -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);
});

Expand Down Expand Up @@ -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);
});
Expand All @@ -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);
});

Expand All @@ -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);
});

Expand All @@ -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);
Expand Down