Repository navigation
fix: expose WebSocket close metadata in RealtimeEvents.CLOSE handler #862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,13 @@ | |
| if typing.TYPE_CHECKING: | ||
| from websockets.asyncio.client import ClientConnection | ||
|
|
||
| # Import websockets exceptions for close code/reason handling | ||
| try: | ||
| from websockets.exceptions import ConnectionClosed, ConnectionClosedOK, ConnectionClosedError | ||
| except ImportError: | ||
| # Fallback for older versions | ||
| ConnectionClosed = ConnectionClosedOK = ConnectionClosedError = Exception # type: ignore | ||
|
|
||
|
|
||
| class RealtimeEvents(str, Enum): | ||
| """Events emitted by the RealtimeConnection""" | ||
|
|
@@ -71,6 +78,8 @@ def __init__(self, websocket: "ClientConnection", current_sample_rate: int, ffmp | |
| self.ffmpeg_process = ffmpeg_process | ||
| self._event_handlers: typing.Dict[str, typing.List[typing.Callable]] = {} | ||
| self._message_task: typing.Optional[asyncio.Task] = None | ||
| self._close_code: typing.Optional[int] = None | ||
| self._close_reason: typing.Optional[str] = None | ||
|
|
||
| def on(self, event: str, callback: typing.Callable) -> None: | ||
| """ | ||
|
|
@@ -87,6 +96,10 @@ def handle_transcript(data): | |
|
|
||
| connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, handle_transcript) | ||
| ``` | ||
|
|
||
| Note: | ||
| For RealtimeEvents.CLOSE, the callback receives two arguments: | ||
| (close_code: int | None, close_reason: str | None) | ||
| """ | ||
| if event not in self._event_handlers: | ||
| self._event_handlers[event] = [] | ||
|
|
@@ -143,10 +156,25 @@ async def _start_message_handler(self) -> None: | |
| pass | ||
| except json.JSONDecodeError as e: | ||
| self._emit(RealtimeEvents.ERROR, {"error": f"Failed to parse message: {e}"}) | ||
| except ConnectionClosed as e: | ||
| # Handle websocket close event to extract close code and reason | ||
| 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: | ||
| # Fallback to sent close frame if received is not available | ||
| self._close_code = e.sent.code | ||
| 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}"}) | ||
| except Exception as e: | ||
| self._emit(RealtimeEvents.ERROR, {"error": str(e)}) | ||
| finally: | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CLOSE metadata missing on normal closeHigh Severity
Reviewed by Cursor Bugbot for commit 7396655. Configure here. |
||
| self._emit(RealtimeEvents.CLOSE, close_code, close_reason) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| async def send(self, data: typing.Dict[str, typing.Any]) -> None: | ||
| """ | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dropped connections skip ERROR event
Medium Severity
The new
except ConnectionClosedblock interceptsConnectionClosedErrorthat previously reachedexcept Exceptionand always emittedERROR. It now emitsERRORonly whene.rcvdis set, so a dropped connection (rcvdisNone) no longer firesERRORand still leaves close metadata empty.Reviewed by Cursor Bugbot for commit 7396655. Configure here.