Skip to content

Commit 0a65546

Browse files
committed
feat(SocketModeReceiver): expose Socket Mode timeout and reconnect options
SocketModeClient accepts clientPingTimeout, serverPingTimeout, pingPongLoggingEnabled, and autoReconnectEnabled, but SocketModeReceiver does not pass these through, forcing users to accept hardcoded defaults. This is problematic because Slack's servers frequently take >5000ms to respond to pings (see #2496), causing unnecessary disconnections with the default 5000ms clientPingTimeout. Changes: - Add clientPingTimeout, serverPingTimeout, pingPongLoggingEnabled, and autoReconnectEnabled to SocketModeReceiverOptions interface - Pass these options through to the SocketModeClient constructor - Add tests verifying option passthrough and backward compatibility Fixes #2496
1 parent 128100c commit 0a65546

File tree

2 files changed

+122
-0
lines changed

2 files changed

+122
-0
lines changed

src/receivers/SocketModeReceiver.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export interface SocketModeReceiverOptions {
3939
installerOptions?: InstallerOptions;
4040
appToken: string; // App Level Token
4141
customRoutes?: CustomRoute[];
42+
clientPingTimeout?: number;
43+
serverPingTimeout?: number;
44+
pingPongLoggingEnabled?: boolean;
45+
autoReconnectEnabled?: boolean;
4246
// biome-ignore lint/suspicious/noExplicitAny: user-provided custom properties can be anything
4347
customPropertiesExtractor?: (args: any) => StringIndexed;
4448
processEventErrorHandler?: (args: SocketModeReceiverProcessEventErrorHandlerArgs) => Promise<boolean>;
@@ -96,6 +100,10 @@ export default class SocketModeReceiver implements Receiver {
96100
appToken,
97101
logger = undefined,
98102
logLevel = LogLevel.INFO,
103+
clientPingTimeout = undefined,
104+
serverPingTimeout = undefined,
105+
pingPongLoggingEnabled = undefined,
106+
autoReconnectEnabled = undefined,
99107
clientId = undefined,
100108
clientSecret = undefined,
101109
stateSecret = undefined,
@@ -111,6 +119,10 @@ export default class SocketModeReceiver implements Receiver {
111119
appToken,
112120
logLevel,
113121
logger,
122+
clientPingTimeout,
123+
serverPingTimeout,
124+
pingPongLoggingEnabled,
125+
autoReconnectEnabled,
114126
clientOptions: installerOptions.clientOptions,
115127
});
116128

test/unit/receivers/SocketModeReceiver.spec.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { EventEmitter } from 'node:events';
12
import { IncomingMessage, ServerResponse } from 'node:http';
23
import path from 'node:path';
34
import { InstallProvider } from '@slack/oauth';
@@ -96,6 +97,115 @@ describe('SocketModeReceiver', () => {
9697
});
9798
assert.isNotNull(receiver);
9899
});
100+
it('should pass clientPingTimeout to SocketModeClient', async () => {
101+
const constructorSpy = sinon.spy();
102+
class FakeSocketModeClient extends EventEmitter {
103+
// biome-ignore lint/suspicious/noExplicitAny: test mock
104+
constructor(opts: any) {
105+
super();
106+
constructorSpy(opts);
107+
}
108+
}
109+
const smOverrides = mergeOverrides(overrides, {
110+
'@slack/socket-mode': {
111+
SocketModeClient: FakeSocketModeClient,
112+
},
113+
});
114+
const SocketModeReceiver = importSocketModeReceiver(smOverrides);
115+
const receiver = new SocketModeReceiver({
116+
appToken: 'my-secret',
117+
logger: noopLogger,
118+
clientPingTimeout: 15000,
119+
});
120+
assert.isNotNull(receiver);
121+
sinon.assert.calledOnce(constructorSpy);
122+
const constructorArgs = constructorSpy.firstCall.args[0];
123+
assert.equal(constructorArgs.clientPingTimeout, 15000);
124+
});
125+
it('should pass serverPingTimeout to SocketModeClient', async () => {
126+
const constructorSpy = sinon.spy();
127+
class FakeSocketModeClient extends EventEmitter {
128+
// biome-ignore lint/suspicious/noExplicitAny: test mock
129+
constructor(opts: any) {
130+
super();
131+
constructorSpy(opts);
132+
}
133+
}
134+
const smOverrides = mergeOverrides(overrides, {
135+
'@slack/socket-mode': {
136+
SocketModeClient: FakeSocketModeClient,
137+
},
138+
});
139+
const SocketModeReceiver = importSocketModeReceiver(smOverrides);
140+
const receiver = new SocketModeReceiver({
141+
appToken: 'my-secret',
142+
logger: noopLogger,
143+
serverPingTimeout: 60000,
144+
});
145+
assert.isNotNull(receiver);
146+
sinon.assert.calledOnce(constructorSpy);
147+
const constructorArgs = constructorSpy.firstCall.args[0];
148+
assert.equal(constructorArgs.serverPingTimeout, 60000);
149+
});
150+
it('should pass all socket mode timeout options to SocketModeClient', async () => {
151+
const constructorSpy = sinon.spy();
152+
class FakeSocketModeClient extends EventEmitter {
153+
// biome-ignore lint/suspicious/noExplicitAny: test mock
154+
constructor(opts: any) {
155+
super();
156+
constructorSpy(opts);
157+
}
158+
}
159+
const smOverrides = mergeOverrides(overrides, {
160+
'@slack/socket-mode': {
161+
SocketModeClient: FakeSocketModeClient,
162+
},
163+
});
164+
const SocketModeReceiver = importSocketModeReceiver(smOverrides);
165+
const receiver = new SocketModeReceiver({
166+
appToken: 'my-secret',
167+
logger: noopLogger,
168+
clientPingTimeout: 15000,
169+
serverPingTimeout: 60000,
170+
pingPongLoggingEnabled: true,
171+
autoReconnectEnabled: false,
172+
});
173+
assert.isNotNull(receiver);
174+
sinon.assert.calledOnce(constructorSpy);
175+
const constructorArgs = constructorSpy.firstCall.args[0];
176+
assert.equal(constructorArgs.clientPingTimeout, 15000);
177+
assert.equal(constructorArgs.serverPingTimeout, 60000);
178+
assert.equal(constructorArgs.pingPongLoggingEnabled, true);
179+
assert.equal(constructorArgs.autoReconnectEnabled, false);
180+
});
181+
it('should use defaults when socket mode timeout options are not provided', async () => {
182+
const constructorSpy = sinon.spy();
183+
class FakeSocketModeClient extends EventEmitter {
184+
// biome-ignore lint/suspicious/noExplicitAny: test mock
185+
constructor(opts: any) {
186+
super();
187+
constructorSpy(opts);
188+
}
189+
}
190+
const smOverrides = mergeOverrides(overrides, {
191+
'@slack/socket-mode': {
192+
SocketModeClient: FakeSocketModeClient,
193+
},
194+
});
195+
const SocketModeReceiver = importSocketModeReceiver(smOverrides);
196+
const receiver = new SocketModeReceiver({
197+
appToken: 'my-secret',
198+
logger: noopLogger,
199+
});
200+
assert.isNotNull(receiver);
201+
sinon.assert.calledOnce(constructorSpy);
202+
const constructorArgs = constructorSpy.firstCall.args[0];
203+
assert.equal(constructorArgs.appToken, 'my-secret');
204+
assert.isUndefined(constructorArgs.clientPingTimeout);
205+
assert.isUndefined(constructorArgs.serverPingTimeout);
206+
assert.isUndefined(constructorArgs.pingPongLoggingEnabled);
207+
assert.isUndefined(constructorArgs.autoReconnectEnabled);
208+
});
99209
it('should throw an error if redirect uri options supplied invalid or incomplete', async () => {
100210
const SocketModeReceiver = importSocketModeReceiver(overrides);
101211
const clientId = 'my-clientId';

0 commit comments

Comments
 (0)