From 5382b88de6deb5efe298d978fa20c0fbe6730e60 Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Wed, 2 Sep 2026 13:09:59 -0400 Subject: [PATCH 1/2] fix: stop aiohttp Socket Mode connect() retrying forever after close (#1913) SocketModeClient.connect() used `while True` and its retry handler never checked self.closed, so once close() closed the aiohttp ClientSession the loop spun forever on `RuntimeError: Session is closed` (observed as 270k+ errors over 46 days in production). Make connect() respect self.closed like its sibling loops (monitor_current_session, receive_messages) already do: guard the loop with `while not self.closed` and return from the retry handler once shutdown has begun. Co-Authored-By: Claude --- slack_sdk/socket_mode/aiohttp/__init__.py | 6 +++++- tests/slack_sdk_async/socket_mode/test_aiohttp.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/slack_sdk/socket_mode/aiohttp/__init__.py b/slack_sdk/socket_mode/aiohttp/__init__.py index a31fd2c15..da104b50e 100644 --- a/slack_sdk/socket_mode/aiohttp/__init__.py +++ b/slack_sdk/socket_mode/aiohttp/__init__.py @@ -349,7 +349,7 @@ async def connect(self): # a new monitor and a new message receiver are also created. # If a new session is created but we failed to create the new # monitor or the new message, we should try it. - while True: + while not self.closed: try: old_session: Optional[ClientWebSocketResponse] = ( None if self.current_session is None else self.current_session @@ -405,6 +405,10 @@ async def connect(self): self.logger.debug(f"A new receive_messages() executor has been recreated for {session_id}") break except Exception as e: + if self.closed: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Stopped connecting because the client is closed (error: {e})") + return self.logger.exception(f"Failed to connect (error: {e}); Retrying...") await asyncio.sleep(self.ping_interval) diff --git a/tests/slack_sdk_async/socket_mode/test_aiohttp.py b/tests/slack_sdk_async/socket_mode/test_aiohttp.py index eda1fecdd..2b5d4adc4 100644 --- a/tests/slack_sdk_async/socket_mode/test_aiohttp.py +++ b/tests/slack_sdk_async/socket_mode/test_aiohttp.py @@ -31,6 +31,20 @@ async def test_init_close(self): finally: await client.close() + @async_test + async def test_connect_returns_when_closed(self): + # Regression test for #1913: connect() must not loop forever once the client is closed. + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + auto_reconnect_enabled=False, + ping_interval=0.01, + ) + client.wss_uri = "ws://localhost:8888/link" + await client.close() + await asyncio.wait_for(client.connect(), timeout=1.0) + self.assertTrue(client.closed) + @async_test async def test_init_with_loop(self): client = SocketModeClient( From 13bca88db838075eba8989aaff0d42cc8774ce37 Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Wed, 2 Sep 2026 13:57:56 -0400 Subject: [PATCH 2/2] test(socket-mode): cover connect() close-during-exception guard Regression coverage for #1913: assert the aiohttp Socket Mode connect() retry handler returns without logging or retrying when an in-flight attempt raises after close(). The existing test only exercised the `while not self.closed` loop condition, not the except-block guard. Co-Authored-By: Claude --- .../socket_mode/test_aiohttp.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/slack_sdk_async/socket_mode/test_aiohttp.py b/tests/slack_sdk_async/socket_mode/test_aiohttp.py index 2b5d4adc4..4834c0c84 100644 --- a/tests/slack_sdk_async/socket_mode/test_aiohttp.py +++ b/tests/slack_sdk_async/socket_mode/test_aiohttp.py @@ -1,5 +1,7 @@ import asyncio +import logging import unittest +from unittest.mock import MagicMock from slack_sdk.socket_mode.aiohttp import SocketModeClient from slack_sdk.web.async_client import AsyncWebClient @@ -45,6 +47,27 @@ async def test_connect_returns_when_closed(self): await asyncio.wait_for(client.connect(), timeout=1.0) self.assertTrue(client.closed) + @async_test + async def test_connect_returns_when_exception_raised_after_close(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + auto_reconnect_enabled=False, + ping_interval=0.01, + ) + client.logger = MagicMock() + client.logger.level = logging.DEBUG + + async def close_then_raise(*args, **kwargs): + await client.close() + raise RuntimeError("Session is closed") + + client.issue_new_wss_url = close_then_raise + + await asyncio.wait_for(client.connect(), timeout=1.0) + self.assertTrue(client.closed) + client.logger.exception.assert_not_called() + @async_test async def test_init_with_loop(self): client = SocketModeClient(