From 2253dfff133bf606ffc69fa05a1da3c52f5049c4 Mon Sep 17 00:00:00 2001 From: Grmiade Date: Thu, 13 Aug 2026 19:52:23 +0200 Subject: [PATCH] fix(socket-mode): suppress heartbeat warnings during shutdown --- .changeset/socket-mode-shutdown-heartbeat.md | 5 +++ .../socket-mode/src/SlackWebSocket.test.ts | 35 +++++++++++++++++++ packages/socket-mode/src/SlackWebSocket.ts | 7 +++- packages/socket-mode/src/SocketModeClient.ts | 4 ++- 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 .changeset/socket-mode-shutdown-heartbeat.md diff --git a/.changeset/socket-mode-shutdown-heartbeat.md b/.changeset/socket-mode-shutdown-heartbeat.md new file mode 100644 index 000000000..b8326547e --- /dev/null +++ b/.changeset/socket-mode-shutdown-heartbeat.md @@ -0,0 +1,5 @@ +--- +'@slack/socket-mode': patch +--- + +Suppress heartbeat timeout warnings while a Socket Mode client intentionally closes its WebSocket connection. diff --git a/packages/socket-mode/src/SlackWebSocket.test.ts b/packages/socket-mode/src/SlackWebSocket.test.ts index 5a3563bcb..9d611e992 100644 --- a/packages/socket-mode/src/SlackWebSocket.test.ts +++ b/packages/socket-mode/src/SlackWebSocket.test.ts @@ -92,5 +92,40 @@ describe('SlackWebSocket', () => { ws.dispatchEvent(new ErrorEvent('error', { error: new Error('boom'), message: 'boom' })); sinon.assert.calledOnce(discStub); }); + + it('should stop heartbeat monitoring during an intentional disconnect', () => { + const ws = new WSMock(); + SlackWebSocket = proxyquire.load('./SlackWebSocket', { + undici: { + WebSocket: class Fake { + constructor() { + // biome-ignore lint/correctness/noConstructorReturn: for test mocking purposes + return ws; + } + }, + CloseEvent, + ErrorEvent, + MessageEvent, + ping: () => {}, + }, + }).SlackWebSocket; + const logger = new ConsoleLogger(); + const warn = sinon.stub(logger, 'warn'); + const clock = sandbox.useFakeTimers(); + const sws = new SlackWebSocket({ + url: 'whatevs', + client: new EventEmitter(), + clientPingTimeoutMS: 5000, + serverPingTimeoutMS: 30000, + logger, + }); + + sws.connect(); + ws.dispatchEvent(new Event('open')); + sws.disconnect({ suppressHeartbeat: true }); + clock.tick(20000); + + sinon.assert.neverCalledWithMatch(warn, "A pong wasn't received from the server"); + }); }); }); diff --git a/packages/socket-mode/src/SlackWebSocket.ts b/packages/socket-mode/src/SlackWebSocket.ts index 3fcb24a13..5b1cbb85a 100644 --- a/packages/socket-mode/src/SlackWebSocket.ts +++ b/packages/socket-mode/src/SlackWebSocket.ts @@ -208,7 +208,12 @@ export class SlackWebSocket { /** * Disconnects the WebSocket connection with Slack, if connected. */ - public disconnect(): void { + public disconnect({ suppressHeartbeat = false }: { suppressHeartbeat?: boolean } = {}): void { + if (suppressHeartbeat) { + clearTimeout(this.serverPingTimeout); + clearInterval(this.clientPingTimeout); + } + if (this.websocket) { // Disconnecting a WebSocket involves a close frame handshake so we check if we've already received a close frame. // If so, we can terminate the underlying socket connection and let the client know. diff --git a/packages/socket-mode/src/SocketModeClient.ts b/packages/socket-mode/src/SocketModeClient.ts index 9f07c58c4..a2611b7cd 100644 --- a/packages/socket-mode/src/SocketModeClient.ts +++ b/packages/socket-mode/src/SocketModeClient.ts @@ -231,7 +231,9 @@ export class SocketModeClient extends EventEmitter { } else { // Resolve (or reject) on disconnect this.once(State.Disconnected, resolve); - this.websocket?.disconnect(); + // A manual shutdown should not report heartbeat failures while the + // WebSocket close handshake is in progress. + this.websocket?.disconnect({ suppressHeartbeat: true }); } }); }