Skip to content

fix(socket-mode): stop waiting on a close handshake Slack never completes - #2819

Merged
WilliamBergamin merged 6 commits into
mainfrom
fix-issue-2752
Nov 6, 2026
Merged

WilliamBergamin merged 6 commits into
mainfrom
fix-issue-2752

Conversation

@WilliamBergamin

Copy link
Copy Markdown
Contributor

Summary

This pull request stops disconnect() from waiting 5 seconds or more on a close handshake that Slack never completes.

Fixes #2752. Supersedes #2697 (thanks @Grmiade, credited as co-author).

The cause: undici marks the socket CLOSING once the close frames are exchanged, but only fires close when the peer ends TCP. Slack never does, so disconnect() waited until the pong watchdog gave up (about clientPingTimeout, with the A pong wasn't received warning) or until the 30s fallback timer.

  • 🟢 The close-handshake timeout drops from 30s to 1s, logged at debug since it is the expected path against Slack.
  • 🟢 The ping/pong heartbeat stops once the close frame is sent, so the timeout owns the wait and the false pong warning is gone.
  • 🟢 When Slack starts the close handshake, the socket is terminated on the next heartbeat tick. undici's ping() silently does nothing while CLOSING, so before this only the watchdog caught it.

Why 1s and not 0: the wait lets acks queued just before disconnect() (bolt ack() then app.stop()) reach Slack before the socket is destroyed, and it still works with a custom dispatcher, where the SDK can't reach the underlying socket.

Testing

The updated and new unit tests fail on main.

To check by hand against a build of this branch:

Requirements

🤖 Generated with Claude Code

…etes

undici marks the socket CLOSING when the close handshake completes but only
fires "close" once the peer ends TCP, which Slack never does. disconnect()
therefore waited for the pong watchdog (about clientPingTimeout) or the 30s
fallback timer.

- Lower the close-handshake timeout to 1s and log it at debug: it is the
  expected path against Slack.
- Stop the ping/pong heartbeat once the close frame is sent, so the timeout
  owns the wait and the spurious "A pong wasn't received" warning is gone.
- When Slack starts the close handshake, terminate on the next heartbeat
  tick; undici's ping() silently does nothing while CLOSING.

Fixes #2752. Supersedes #2697.

Co-authored-by: Grmiade <jeremy@payfit.com>
Co-authored-by: Claude <svc-devxp-claude@slack-corp.com>
@changeset-bot

changeset-bot Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 76c94cc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@slack/socket-mode Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.90%. Comparing base (822f6a4) to head (76c94cc).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2819   +/-   ##
=======================================
  Coverage   93.89%   93.90%           
=======================================
  Files          66       66           
  Lines       10571    10581   +10     
  Branches      611      611           
=======================================
+ Hits         9926     9936   +10     
- Misses        609      611    +2     
+ Partials       36       34    -2     
Flag Coverage Δ
cli-hooks 93.84% <100.00%> (+0.09%) ⬆️
cli-test 93.84% <100.00%> (+0.09%) ⬆️
logger 93.84% <100.00%> (+0.09%) ⬆️
oauth 93.84% <100.00%> (+0.09%) ⬆️
socket-mode 93.84% <100.00%> (+0.09%) ⬆️
web-api 93.84% <100.00%> (+0.09%) ⬆️
webhook 93.84% <100.00%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

WilliamBergamin and others added 3 commits November 5, 2026 12:55
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
…ake short

The test still passed with stopMonitoringPings() removed from disconnect(),
since the heartbeat's CLOSING check cleaned up early with no ping or warn.
Asserting no close before the 1s timeout guards the ack window, and the
test name now says what it checks.

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
@WilliamBergamin WilliamBergamin self-assigned this Nov 5, 2026
@WilliamBergamin WilliamBergamin added bug M-T: A confirmed bug report. Issues are confirmed when the reproduction steps are documented semver:patch pkg:socket-mode applies to `@slack/socket-mode` labels Nov 5, 2026
@WilliamBergamin
WilliamBergamin marked this pull request as ready for review November 5, 2026 20:50
@WilliamBergamin
WilliamBergamin requested a review from a team as a code owner November 5, 2026 20:50
@zimeg zimeg added this to the socket-mode@next milestone Nov 6, 2026

@zimeg zimeg left a comment

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.

🔌 @WilliamBergamin LGTM! Stopping the heartbeat once the close frame is sent is a clean fix, and thanks for the tests that fail on main ✨

@mwbrooks mwbrooks left a comment

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.

✅ Thanks @WilliamBergamin! Taking app.stop() from 5s down to about 1s is a lovely quality-of-life fix for Bolt apps.

this.logger.error('WebSocket not available, skipping ping.');
return;
}
if (this.websocket.readyState === WebSocket.CLOSING) {

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: Terminating on the next heartbeat tick is a neat answer to undici's silent ping() while CLOSING, and disconnect() already routes that state straight to cleanup().

@WilliamBergamin
WilliamBergamin merged commit 1d3a1af into main Nov 6, 2026
22 checks passed
@WilliamBergamin
WilliamBergamin deleted the fix-issue-2752 branch November 6, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug M-T: A confirmed bug report. Issues are confirmed when the reproduction steps are documented pkg:socket-mode applies to `@slack/socket-mode` semver:patch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SocketModeClient.disconnect() takes ~5 seconds to close on 3.0.1

3 participants