Repository navigation
Description
Summary
KalshiWebSocket._start() (kalshi/ws/client.py:124-148) unconditionally re-instantiates _connection, _sub_mgr, _seq_tracker, _orderbook_mgr, and _dispatcher. The _WebSocketSession.__aenter__ wrapper (lines 979-981) just calls _start() without checking prior state. Result: nesting or re-using a single KalshiWebSocket instance silently clobbers any subscriptions / recv task / sequence tracker that the outer session was relying on, leaking the in-flight task and orphaning the user's consumer iterators.
Reproducer
import asyncio
from kalshi import KalshiAuth, KalshiConfig
from kalshi.ws import KalshiWebSocket
async def main():
auth = KalshiAuth.from_key_path("kid", "key.pem")
ws = KalshiWebSocket(auth=auth, config=KalshiConfig.demo())
# Pattern 1: accidental nested re-entry (e.g. helper function picked up a
# cached `ws` from outer scope)
async with ws.connect() as outer:
stream_a = await outer.subscribe_ticker(tickers=["AAA"])
async with ws.connect() as inner: # ← silently clobbers outer's state
stream_b = await inner.subscribe_ticker(tickers=["BBB"])
# stream_a is now orphaned — its sid is no longer in the new sub_mgr
# outer.__aexit__ closes the freshly-replaced connection; stream_a never
# exits its `async for` (no sentinel was put on the orphaned queue).Pattern 2 (more realistic): a long-lived service holds a KalshiWebSocket as an attribute, reconnects after a transient error by calling connect() again on the same instance, and the prior subscriptions disappear without an explicit error.
Suggested fix
Guard _start() so a second entry fails loudly:
async def _start(self) -> None:
"""Connect and initialize managers. Does NOT start recv_loop yet."""
if self._connection is not None or self._running:
raise RuntimeError(
"KalshiWebSocket session is already started. Each WS instance "
"supports exactly one active session — create a new "
"KalshiWebSocket() for a new session, or exit the existing "
"`async with ws.connect()` first."
)
self._connection = ConnectionManager(...)
...Add a regression test in tests/ws/test_client.py:
async def test_double_start_raises(test_auth, config):
ws = KalshiWebSocket(auth=test_auth, config=config)
async with ws.connect():
with pytest.raises(RuntimeError, match="already started"):
async with ws.connect():
passOptionally also assert _stop() clears the managers so a clean re-connect() on the same instance after the prior __aexit__ is still supported (current code does not reset to None in _stop — verify and decide whether to support or forbid).
Discovered by
Post-v2.5 multi-reviewer audit (WebSocket surface).
Activity
fix-up(ws): reset state on _start failure; pin regression test; trim …
fix(ws): guard _start() against re-entry; clear managers on _stop (#297…
release: 2.6.0 — post-v2.5 independent reviewer SDK audit closure (#295-
fix(ws): backpressure error tears down WS cleanly; wake pause waiter (#…