diff --git a/ibind/base/ws_client.py b/ibind/base/ws_client.py index 9805445f..9da4e4a7 100644 --- a/ibind/base/ws_client.py +++ b/ibind/base/ws_client.py @@ -162,7 +162,14 @@ def _run_websocket(self, wsa: WebSocketApp): try: # the timeout is set to a little sooner than the interval - wsa.run_forever(ping_interval=self._ping_interval, ping_timeout=self._ping_interval * 0.95, sslopt=self._sslopt) + # skip_utf8_validation=True: IBKR occasionally emits non-UTF-8 bytes (e.g. 0x99) in text frames, + # which crashes the run_forever thread before on_close fires. Decoding is handled above this layer. + wsa.run_forever( + ping_interval=self._ping_interval, + ping_timeout=self._ping_interval * 0.95, + sslopt=self._sslopt, + skip_utf8_validation=True, + ) except ValueError as e: if 'url is invalid' in str(e): diff --git a/test/integration/base/test_websocket_client_i.py b/test/integration/base/test_websocket_client_i.py index b14d5e75..40801dd2 100644 --- a/test/integration/base/test_websocket_client_i.py +++ b/test/integration/base/test_websocket_client_i.py @@ -95,6 +95,7 @@ def _verify_started(ws_client: WsClient, wsa_mock: MagicMock): sslopt=ws_client._sslopt, ping_interval=ws_client._ping_interval, ping_timeout=0.95 * ws_client._ping_interval, + skip_utf8_validation=True, ) wsa_mock._on_open.assert_called_with(wsa_mock) @@ -260,6 +261,7 @@ def run_forever_exception( sslopt: dict = None, ping_interval: float = 0, ping_timeout: Optional[float] = None, + skip_utf8_validation: bool = False, ): wsa_mock.run_forever.side_effect = old_run_forever raise RuntimeError(_ERROR_MESSAGE) diff --git a/test/integration/base/websocketapp_mock.py b/test/integration/base/websocketapp_mock.py index 5961f7a4..af2458c6 100644 --- a/test/integration/base/websocketapp_mock.py +++ b/test/integration/base/websocketapp_mock.py @@ -41,7 +41,7 @@ def close(wsa_mock: MagicMock, status: str = None): wsa_mock._on_close(wsa_mock, None, None) -def run_forever(wsa_mock: MagicMock, sslopt: dict = None, ping_interval: float = 0, ping_timeout: Optional[float] = None): +def run_forever(wsa_mock: MagicMock, sslopt: dict = None, ping_interval: float = 0, ping_timeout: Optional[float] = None, skip_utf8_validation: bool = False): wsa_mock.keep_running = True wsa_mock._on_open(wsa_mock)