Prevent zombie Slack Socket Mode connections on reconnect
Cluster 18980. Data as of
Nothing open on GitHub. Fix merged in #19181 on 3 May 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #19181. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #19181. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and PRs address a bug where calling connect() in Slack Socket Mode leaves previous handlers or tasks open, creating zombie connections. Related work also covers hardening socket recovery and deduplicating restart behavior on Windows.
Open issues0
Open PRs0
Fixes on record
-
#19181 fix(slack): close previous handler in connect() to prevent zombie Socket Mode connections
files:
gateway/platforms/slack.py,scripts/release.py,tests/gateway/test_slack.pyShow the diff
-
gateway/platforms/slack.py@@ -528,6 +528,21 @@ async def connect(self) -> bool: return False lock_acquired = True + # Close any previous handler before creating a new one so that + # calling connect() a second time (e.g. during a gateway restart or + # in-process reconnect attempt) does not leave a zombie Socket Mode + # connection alive. Both the old and new connections would otherwise + # receive every Slack event and dispatch it twice, producing double + # responses — the same bug that affected DiscordAdapter (#18187). + if self._handler is not None: + try: + await self._handler.close_async() + except Exception: + logger.debug("[%s] Failed to close previous Slack handler", self.name) + finally: + self._handler = None + self._app = None + # First token is the primary — used for AsyncApp / Socket Mode primary_token = [redacted:secret-assignment] self._app = AsyncApp(token=[redacted:secret-assignment] -
scripts/release.py@@ -506,6 +506,7 @@ "[redacted:email]": "gnanam1990", "[redacted:email]": "0xyg3n", "[email protected]": "0xyg3n", + "[redacted:email]": "nftpoetrist", # PR #18982 "[redacted:email]": "Hypn0sis", "[redacted:email]": "OwenYWT", "[redacted:email]": "v1k22", -
tests/gateway/test_slack.py@@ -231,6 +231,55 @@ async def test_releases_platform_lock_when_auth_fails(self): mock_release.assert_called_once_with("slack-app-token", "xapp-fake") assert adapter._platform_lock_identity is None + @pytest.mark.asyncio + async def test_reconnect_closes_previous_handler_to_prevent_zombie_socket(self): + """Regression for #18980: calling connect() on an adapter that already has + a live handler (e.g. during a gateway restart) must close the old + AsyncSocketModeHandler before creating a new one. Without this guard, + the old Socket Mode websocket stays alive and both connections dispatch + every Slack event, producing double responses — the same bug that + affected DiscordAdapter (#18187). + """ + config = PlatformConfig(enabled=True, token="[redacted:secret-assignment]") + adapter = SlackAdapter(config) + + # Simulate state left over from a prior connect() call. + first_handler = AsyncMock() + first_handler.close_async = AsyncMock() + adapter._handler = first_handler + + mock_app = MagicMock() + def _noop_decorator(event_type): + def decorator(fn): return fn + return decorator + mock_app.event = _noop_decorator + mock_app.command = _noop_decorator + mock_app.action = _noop_decorator + mock_app.client = AsyncMock() + + mock_web_client = AsyncMock() + mock_web_client.auth_test = AsyncMock(return_value={ + "user_id": "U_BOT", + "user": "testbot", + "team_id": "T_FAKE", + "team": "FakeTeam", + }) + + second_handler = MagicMock() + + with patch.object(_slack_mod, "AsyncApp", return_value=mock_app), \ + patch.object(_slack_mod, "AsyncWebClient", return_value=mock_web_client), \ + patch.object(_slack_mod, "AsyncSocketModeHandler", return_value=second_handler), \ + patch.dict(os.environ, {"SLACK_APP_TOKEN": "[redacted:secret-assignment]"}), \ + patch("gateway.status.acquire_scoped_lock", return_value=(True, None)), \ + patch("gateway.status.release_scoped_lock"), \ + patch("asyncio.create_task"): + result = await adapter.connect() + + assert result is True + first_handler.close_async.assert_awaited_once_with() + assert adapter._handler is second_handler + # --------------------------------------------------------------------------- # TestSlackProxyBehavior
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#18982 fix(slack): close previous handler in connect() to prevent zombie Socket Mode connections · merged via another change →
#19181
Closed issues · 1
- Closed#18980fix(slack): close previous handler/task in connect() to prevent zombie Socket Mode connections
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #19181
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 3 May 2026