From 0b075d500bc42a5e31a65cccfb815096b5c0dbaf Mon Sep 17 00:00:00 2001 From: Aleksey Levenstein Date: Tue, 20 Oct 2026 19:52:33 +0300 Subject: [PATCH] fix(socket-mode): handle events_api payloads without an inner event Slack sends app_rate_limited notifications as events_api envelopes with no inner event, which crashed the message handler. Emit the payload type instead so apps can listen for it. --- .changeset/socket-mode-app-rate-limited.md | 5 +++ .../socket-mode/src/SocketModeClient.test.ts | 38 +++++++++++++++++++ packages/socket-mode/src/SocketModeClient.ts | 16 +++++++- 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .changeset/socket-mode-app-rate-limited.md diff --git a/.changeset/socket-mode-app-rate-limited.md b/.changeset/socket-mode-app-rate-limited.md new file mode 100644 index 000000000..cfebbdb36 --- /dev/null +++ b/.changeset/socket-mode-app-rate-limited.md @@ -0,0 +1,5 @@ +--- +"@slack/socket-mode": patch +--- + +Fix a crash on `events_api` messages without an inner event ([#2167](https://github.com/slackapi/node-slack-sdk/issues/2167)). Slack sends `app_rate_limited` notifications this way, and the client threw `Cannot read properties of undefined (reading 'type')`. These messages are now emitted under their payload type, e.g. `app_rate_limited`. diff --git a/packages/socket-mode/src/SocketModeClient.test.ts b/packages/socket-mode/src/SocketModeClient.test.ts index e68ff270f..f9529646f 100644 --- a/packages/socket-mode/src/SocketModeClient.test.ts +++ b/packages/socket-mode/src/SocketModeClient.test.ts @@ -259,6 +259,20 @@ describe('SocketModeClient', () => { retry_attempt: 2, retry_reason: 'timeout', }); + const appRateLimited = JSON.stringify({ + envelope_id: envelopeId, + payload: { + token: 'verification-token', + api_app_id: 'A111', + minute_rate_limited: 1792487520, + type: 'app_rate_limited', + team_id: 'T111', + }, + type: 'events_api', + accepts_response_payload: false, + retry_attempt: 1, + retry_reason: 'timeout', + }); it('should be sent to the specific and generic event listeners, and should not trip an unrelated event listener', async () => { const client = new SocketModeClient({ appToken: 'xapp-' }); @@ -316,6 +330,30 @@ describe('SocketModeClient', () => { client.emit('ws_message', message, false /* isBinary */); sinon.assert.callCount(spy, 1); }); + it('should send payloads without an inner event to the payload type and slack_event listeners', async () => { + const client = new SocketModeClient({ appToken: 'xapp-' }); + const rateLimitedSpy = sinon.spy(); + const slackEventSpy = sinon.spy(); + client.on('app_rate_limited', rateLimitedSpy); + client.on('slack_event', slackEventSpy); + client.emit('ws_message', appRateLimited, false /* isBinary */); + await sleep(30); + sinon.assert.calledOnce(rateLimitedSpy); + sinon.assert.calledOnce(slackEventSpy); + }); + it('should pass all the properties to listeners of payloads without an inner event', async () => { + const client = new SocketModeClient({ appToken: 'xapp-' }); + const spy = sinon.spy(); + client.on('app_rate_limited', spy); + client.emit('ws_message', appRateLimited, false /* isBinary */); + await sleep(30); + const args = spy.firstCall.args[0]; + assert.strictEqual(args.envelope_id, envelopeId); + assert.strictEqual(args.body.minute_rate_limited, 1792487520); + assert.strictEqual(args.retry_num, 1); + assert.strictEqual(args.retry_reason, 'timeout'); + assert.strictEqual('event' in args, false); + }); }); describe('interactivity messages', () => { diff --git a/packages/socket-mode/src/SocketModeClient.ts b/packages/socket-mode/src/SocketModeClient.ts index 93bf8c7c0..69b9128aa 100644 --- a/packages/socket-mode/src/SocketModeClient.ts +++ b/packages/socket-mode/src/SocketModeClient.ts @@ -362,7 +362,7 @@ export class SocketModeClient extends EventEmitter { }; // For events_api messages, expose the type of the event - if (event.type === 'events_api') { + if (event.type === 'events_api' && event.payload?.event?.type) { this.emit(event.payload.event.type, { ack, envelope_id: event.envelope_id, @@ -372,6 +372,20 @@ export class SocketModeClient extends EventEmitter { retry_reason: event.retry_reason, accepts_response_payload: event.accepts_response_payload, }); + } else if (event.type === 'events_api') { + // Some events_api payloads carry no inner event, e.g. app_rate_limited + if (event.payload?.type) { + this.emit(event.payload.type, { + ack, + envelope_id: event.envelope_id, + body: event.payload, + retry_num: event.retry_attempt, + retry_reason: event.retry_reason, + accepts_response_payload: event.accepts_response_payload, + }); + } else { + this.logger.debug(`events_api message has no event or payload type: ${event.envelope_id}`); + } } else { // Emit just ack and body for all other types of messages this.emit(event.type, {