Skip to content

fix: expose WebSocket close metadata in RealtimeEvents.CLOSE handler - #862

Open
Shreyaaaash wants to merge 2 commits into
elevenlabs:mainfrom
Shreyaaaash:fix/realtime-close-metadata
Open

Shreyaaaash wants to merge 2 commits into
elevenlabs:mainfrom
Shreyaaaash:fix/realtime-close-metadata

Conversation

@Shreyaaaash

Copy link
Copy Markdown
  • 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
@cursor

cursor Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

PR Summary

Low Risk
Localized realtime connection lifecycle change; CLOSE callbacks that take zero parameters may need updating to accept the new arguments.

Overview
Realtime CLOSE handlers now receive WebSocket close metadata so callers can tell clean shutdowns from failures.

When the message loop ends, the connection captures close_code and close_reason from websockets ConnectionClosed (received or sent close frame, with fallbacks on the socket object), documents the (close_code, close_reason) callback shape on on(), and passes those values into RealtimeEvents.CLOSE instead of emitting with no arguments. Abnormal closes (ConnectionClosedError) also trigger an ERROR event with code and reason.

Reviewed by Cursor Bugbot for commit 7396655. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/elevenlabs/realtime/connection.py
… 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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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}"})

Copy link
Copy Markdown

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 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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7396655. Configure here.

@Shreyaaaash

Copy link
Copy Markdown
Author

Addressing Cursor Bot's Version Compatibility Concern

Cursor Bot correctly pointed out that the initial implementation relied on websocket.close_code and websocket.close_reason, which were introduced in websockets 14.1. Since the project supports websockets >=13.0, those attributes cannot be relied upon.

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 ConnectionClosed exception and extracts the close information from the exception itself:

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 (_close_code and _close_reason) so they remain available to the finally block when the CLOSE event is emitted.

Why this is compatible

The rcvd and sent attributes on ConnectionClosed exceptions are available in the supported websockets versions, including the project's minimum version of 13.0. Therefore, the implementation no longer depends on the websocket.close_code and websocket.close_reason attributes introduced in 14.1.

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

CLOSE event handlers now receive:

close_code: int | None
close_reason: str | None

This allows applications to distinguish between normal and abnormal WebSocket closures:

  • close_code == 1000 — normal provider-initiated closure.
  • close_code != 1000 — abnormal closure that may require recovery or error handling.
  • close_reason — the provider's close message, when available.

This approach preserves the required close metadata while remaining compatible with the project's websockets >=13.0 dependency constraint.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant