Repository navigation
fix: expose WebSocket close metadata in RealtimeEvents.CLOSE handler - #862
Shreyaaaash wants to merge 2 commits into
Conversation
Shreyaaaash
commented
Sep 6, 2026
- Pass close_code and close_reason to CLOSE event handlers
- Update docstring to document the new callback signature for CLOSE events
- Fixes issue where CLOSE handlers received zero arguments, making it impossible to distinguish normal closes from errors
- Pass close_code and close_reason to CLOSE event handlers - Update docstring to document the new callback signature for CLOSE events - Fixes issue where CLOSE handlers received zero arguments, making it impossible to distinguish normal closes from errors
PR SummaryLow Risk Overview When the message loop ends, the connection captures Reviewed by Cursor Bugbot for commit 7396655. Bugbot is set up for automated code reviews on this repo. Configure here. |
… handler - Add exception handling for ConnectionClosed to extract close code and reason - Works with websockets >=13.0 by catching ConnectionClosed exceptions - Extracts close information from received or sent close frames - Stores close code/reason in instance variables for use in finally block - Emits ERROR event for abnormal closures (ConnectionClosedError) - Updates docstring to document CLOSE handler signature - Fixes issue where CLOSE handlers received zero arguments, making it impossible to distinguish normal closes from errors
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
Reviewed by Cursor Bugbot for commit 7396655. Configure here.
| self._emit(RealtimeEvents.CLOSE) | ||
| # Use the stored close code and reason, falling back to websocket attributes if available | ||
| close_code = self._close_code if self._close_code is not None else getattr(self.websocket, 'close_code', None) | ||
| close_reason = self._close_reason if self._close_reason is not None else getattr(self.websocket, 'close_reason', None) |
There was a problem hiding this comment.
CLOSE metadata missing on normal close
High Severity
async for on the asyncio client swallows ConnectionClosedOK, so the new handler never records a clean close. _close_code and _close_reason stay unset, and the close_code fallback is missing on ClientConnection before websockets 14.1. CLOSE still receives None, None on supported 13.x, so normal closes remain indistinguishable from drops.
Reviewed by Cursor Bugbot for commit 7396655. Configure here.
| self._close_reason = e.sent.reason | ||
| # Emit error if this was an abnormal closure | ||
| if isinstance(e, ConnectionClosedError) and e.rcvd is not None: | ||
| self._emit(RealtimeEvents.ERROR, {"error": f"WebSocket connection closed with error: {e.rcvd.code} {e.rcvd.reason}"}) |
There was a problem hiding this comment.
Dropped connections skip ERROR event
Medium Severity
The new except ConnectionClosed block intercepts ConnectionClosedError that previously reached except Exception and always emitted ERROR. It now emits ERROR only when e.rcvd is set, so a dropped connection (rcvd is None) no longer fires ERROR and still leaves close metadata empty.
Reviewed by Cursor Bugbot for commit 7396655. Configure here.
Addressing Cursor Bot's Version Compatibility ConcernCursor Bot correctly pointed out that the initial implementation relied on The implementation has been updated to address this compatibility issue. Instead of accessing close metadata directly from the WebSocket object, the updated implementation catches the except ConnectionClosed as e:
# Extract close metadata from the received close frame.
if hasattr(e, "rcvd") and e.rcvd is not None:
self._close_code = e.rcvd.code
self._close_reason = e.rcvd.reason
elif hasattr(e, "sent") and e.sent is not None:
# Fall back to the sent close frame when no received frame is available.
self._close_code = e.sent.code
self._close_reason = e.sent.reason
# Emit an error for abnormal WebSocket closures.
if isinstance(e, ConnectionClosedError) and e.rcvd is not None:
self._emit(
RealtimeEvents.ERROR,
{
"error": (
f"WebSocket connection closed with error: "
f"{e.rcvd.code} {e.rcvd.reason}"
)
},
)The close code and reason are stored in instance variables ( Why this is compatibleThe A fallback to the WebSocket attributes can still be retained as a defensive measure, but they are no longer part of the primary code path. Result
This allows applications to distinguish between normal and abnormal WebSocket closures:
This approach preserves the required close metadata while remaining compatible with the project's |

