Skip to content

Commit 5c57a32

Browse files
fix: 'ClientConnection' object has no attribute 'closed' (#1613)
* fix: 'ClientConnection' object has no attribute 'closed' * Fix an issue with old version of websockets * Update slack_sdk/socket_mode/websockets/__init__.py Co-authored-by: Kazuhiro Sera <[email protected]> * Add more CI coverage round this issue * Revert flaky tests --------- Co-authored-by: Kazuhiro Sera <[email protected]>
1 parent a7223d9 commit 5c57a32

File tree

1 file changed

+26
-9
lines changed

1 file changed

+26
-9
lines changed

slack_sdk/socket_mode/websockets/__init__.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* https://pypi.org/project/websockets/
66
77
"""
8+
89
import asyncio
910
import logging
1011
from asyncio import Future, Lock
@@ -15,8 +16,13 @@
1516
import websockets
1617
from websockets.exceptions import WebSocketException
1718

18-
# To keep compatibility with websockets 8.x, we use this import over .legacy.client
19-
from websockets import WebSocketClientProtocol
19+
try:
20+
from websockets.asyncio.client import ClientConnection
21+
except ImportError:
22+
# To keep compatibility with websockets <14.x we use WebSocketClientProtocol
23+
# To keep compatibility with websockets 8.x, we use this import over .legacy.client
24+
from websockets import WebSocketClientProtocol as ClientConnection
25+
2026

2127
from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient
2228
from slack_sdk.socket_mode.async_listeners import (
@@ -29,6 +35,17 @@
2935
from ..logger.messages import debug_redacted_message_string
3036

3137

38+
def _session_closed(session: Optional[ClientConnection]) -> bool:
39+
if session is None:
40+
return True
41+
if hasattr(session, "closed"):
42+
# The session is a WebSocketClientProtocol instance
43+
return session.closed
44+
# WebSocket close code, defined in https://datatracker.ietf.org/doc/html/rfc6455.html#section-7.1.5
45+
# None if the connection isn’t closed yet.
46+
return session.close_code is not None
47+
48+
3249
class SocketModeClient(AsyncBaseSocketModeClient):
3350
logger: Logger
3451
web_client: AsyncWebClient
@@ -55,7 +72,7 @@ class SocketModeClient(AsyncBaseSocketModeClient):
5572
ping_interval: float
5673
trace_enabled: bool
5774

58-
current_session: Optional[WebSocketClientProtocol]
75+
current_session: Optional[ClientConnection]
5976
current_session_monitor: Optional[Future]
6077

6178
auto_reconnect_enabled: bool
@@ -105,7 +122,7 @@ async def monitor_current_session(self) -> None:
105122
# In the asyncio runtime, accessing a shared object (self.current_session here) from
106123
# multiple tasks can cause race conditions and errors.
107124
# To avoid such, we access only the session that is active when this loop starts.
108-
session: WebSocketClientProtocol = self.current_session
125+
session: ClientConnection = self.current_session
109126
session_id: str = await self.session_id()
110127
if self.logger.level <= logging.DEBUG:
111128
self.logger.debug(f"A new monitor_current_session() execution loop for {session_id} started")
@@ -117,7 +134,7 @@ async def monitor_current_session(self) -> None:
117134
break
118135
await asyncio.sleep(self.ping_interval)
119136
try:
120-
if self.auto_reconnect_enabled and (session is None or session.closed):
137+
if self.auto_reconnect_enabled and _session_closed(session=session):
121138
self.logger.info(f"The session ({session_id}) seems to be already closed. Reconnecting...")
122139
await self.connect_to_new_endpoint()
123140
except Exception as e:
@@ -134,7 +151,7 @@ async def receive_messages(self) -> None:
134151
# In the asyncio runtime, accessing a shared object (self.current_session here) from
135152
# multiple tasks can cause race conditions and errors.
136153
# To avoid such, we access only the session that is active when this loop starts.
137-
session: WebSocketClientProtocol = self.current_session
154+
session: ClientConnection = self.current_session
138155
session_id: str = await self.session_id()
139156
consecutive_error_count = 0
140157
if self.logger.level <= logging.DEBUG:
@@ -171,15 +188,15 @@ async def receive_messages(self) -> None:
171188
raise
172189

173190
async def is_connected(self) -> bool:
174-
return not self.closed and self.current_session is not None and not self.current_session.closed
191+
return not self.closed and not _session_closed(self.current_session)
175192

176193
async def session_id(self) -> str:
177194
return self.build_session_id(self.current_session)
178195

179196
async def connect(self):
180197
if self.wss_uri is None:
181198
self.wss_uri = await self.issue_new_wss_url()
182-
old_session: Optional[WebSocketClientProtocol] = None if self.current_session is None else self.current_session
199+
old_session: Optional[ClientConnection] = None if self.current_session is None else self.current_session
183200
# NOTE: websockets does not support proxy settings
184201
self.current_session = await websockets.connect(
185202
uri=self.wss_uri,
@@ -250,7 +267,7 @@ async def close(self):
250267
self.message_receiver.cancel()
251268

252269
@classmethod
253-
def build_session_id(cls, session: WebSocketClientProtocol) -> str:
270+
def build_session_id(cls, session: ClientConnection) -> str:
254271
if session is None:
255272
return ""
256273
return "s_" + str(hash(session))

0 commit comments

Comments
 (0)