Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/friendly-sockets-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@slack/socket-mode': patch
---

Fix Events API dispatch for payloads without an inner event, including app_rate_limited. Keep malformed payloads available to generic slack_event listeners for acknowledgement.
98 changes: 98 additions & 0 deletions packages/socket-mode/src/SocketModeClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,104 @@ describe('SocketModeClient', () => {
});

describe('events_api messages', () => {
class TestSocketModeClient extends SocketModeClient {
async receive(payload: unknown): Promise<void> {
await this.onWebSocketMessage(JSON.stringify(payload), false);
}
}

it('should dispatch the inner event of an event_callback', async () => {
const client = new TestSocketModeClient({ appToken: 'xapp-' });
const event = { type: 'app_mention', text: '<@U111>' };
const body = { type: 'event_callback', event };
const specific = sandbox.spy();
const generic = sandbox.spy();
const callback = sandbox.spy();
client.on('app_mention', specific);
client.on('event_callback', callback);
client.on('slack_event', generic);

await client.receive({ type: 'events_api', payload: body, envelope_id: 'inner-event' });

sinon.assert.calledOnce(specific);
sinon.assert.notCalled(callback);
assert.deepEqual(specific.firstCall.args[0].body, body);
assert.deepEqual(specific.firstCall.args[0].event, event);
sinon.assert.calledOnce(generic);
assert.deepEqual(generic.firstCall.args[0].body, body);
});

it('should dispatch app_rate_limited without an inner event', async () => {
const client = new TestSocketModeClient({ appToken: 'xapp-' });
const body = { type: 'app_rate_limited', team_id: 'T111', minute_rate_limited: 1610241741 };
const specific = sandbox.spy();
const generic = sandbox.spy();
client.on('app_rate_limited', specific);
client.on('slack_event', generic);

await client.receive({
type: 'events_api',
payload: body,
envelope_id: 'rate-limited',
retry_attempt: 2,
retry_reason: 'timeout',
accepts_response_payload: false,
});

sinon.assert.calledOnce(specific);
const args = specific.firstCall.args[0];
assert.deepEqual(args.body, body);
assert.strictEqual(args.event, args.body);
assert.strictEqual(args.envelope_id, 'rate-limited');
assert.strictEqual(args.retry_num, 2);
assert.strictEqual(args.retry_reason, 'timeout');
assert.strictEqual(args.accepts_response_payload, false);
sinon.assert.calledOnce(generic);
assert.deepEqual(generic.firstCall.args[0].body, body);
assert.strictEqual(generic.firstCall.args[0].ack, args.ack);
});

it('should allow generic listeners to ACK malformed payloads without a valid event type', async () => {
for (const body of [
{},
{ event: null },
{ event: { type: 123 }, type: 456 },
{ event: { type: '' }, type: '' },
null,
undefined,
]) {
const client = new TestSocketModeClient({ appToken: 'xapp-' });
const send = sandbox
.stub(client as unknown as { send: (id: string, response: unknown) => Promise<void> }, 'send')
.resolves();
const generic = sandbox.spy();
const emit = sandbox.spy(client, 'emit');
client.on('slack_event', generic);

await client.receive({ type: 'events_api', payload: body, envelope_id: 'malformed' });

sinon.assert.calledOnce(emit);
sinon.assert.calledOnce(generic);
const args = generic.firstCall.args[0];
assert.strictEqual(args.type, 'events_api');
assert.deepEqual(args.body, body);
await args.ack({});
sinon.assert.calledOnceWithExactly(send, 'malformed', {});
}
});

it('should use the payload type when the inner event type is invalid', async () => {
const client = new TestSocketModeClient({ appToken: 'xapp-' });
const body = { type: 'app_rate_limited', event: { type: 123 } };
const specific = sandbox.spy();
client.on('app_rate_limited', specific);

await client.receive({ type: 'events_api', payload: body, envelope_id: 'invalid-inner-event' });

sinon.assert.calledOnce(specific);
assert.deepEqual(specific.firstCall.args[0].event, body);
});

const envelopeId = 'cda4159a-72a5-4744-aba3-4d66eb52682b';
const appMention = JSON.stringify({
envelope_id: envelopeId,
Expand Down
25 changes: 16 additions & 9 deletions packages/socket-mode/src/SocketModeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,15 +363,22 @@ export class SocketModeClient extends EventEmitter {

// For events_api messages, expose the type of the event
if (event.type === 'events_api') {
this.emit(event.payload.event.type, {
ack,
envelope_id: event.envelope_id,
body: event.payload,
event: event.payload.event,
retry_num: event.retry_attempt,
retry_reason: event.retry_reason,
accepts_response_payload: event.accepts_response_payload,
});
const innerEvent = event.payload?.event;
const dispatchedEvent =
typeof innerEvent?.type === 'string' && innerEvent.type.length > 0 ? innerEvent : event.payload;
if (typeof dispatchedEvent?.type === 'string' && dispatchedEvent.type.length > 0) {
this.emit(dispatchedEvent.type, {
ack,
envelope_id: event.envelope_id,
body: event.payload,
event: dispatchedEvent,
retry_num: event.retry_attempt,
retry_reason: event.retry_reason,
accepts_response_payload: event.accepts_response_payload,
});

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.

suggestion (non-blocking): Could we debug-log when we skip the specific emit? It'll help someone wondering why their listener never fired.

Suggested change
});
} else {
this.logger.debug(`Received an events_api message without an event type (envelope_id: ${event.envelope_id})`);
}

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.

Addressed in df7676a

} else {
this.logger.debug(`Received an events_api message without an event type (envelope_id: ${event.envelope_id})`);
}
} else {
// Emit just ack and body for all other types of messages
this.emit(event.type, {
Expand Down
Loading