From #106 F-P-16. Severity: medium UX foot-gun (silent no-op).
Behavior
kalshi/ws/client.py only starts _recv_loop from inside _subscribe_via_lock at line 378 (self._ensure_recv_loop()). If a user:
- Constructs
KalshiWebSocket(...),
- Registers callbacks via
ws.on("trade")(handler) / ws.on_error(handler),
- Enters
async with ws.connect() as session:,
- Calls
await session.run_forever(),
…without ever calling a subscribe_* method, run_forever() returns immediately (because self._recv_task is None, see kalshi/ws/client.py:529-532). No messages are ever read from the socket. Server-pushed control frames are also silently dropped.
This is the documented callback-mode workflow in README.md. The README does not say a subscribe is also required, and the type system doesn't catch it.
Options
A. Auto-start _recv_loop on connect. Simple, drains server pushes, but no SID mapping exists so data frames still no-op until subscribe. Doesn't fix the actual signal-loss problem, just the silent-return symptom.
B. Raise in run_forever() if no subscription exists. Loud signal at the call site, no silent hang. Could be a KalshiSubscriptionError("run_forever() requires at least one active subscription").
C. Document only. Update README + add a docstring on run_forever calling out the requirement.
Recommendation
B, with the option to soften to a warning later if it proves too strict. Errors at the obvious failure point beat silent no-ops.
Acceptance
run_forever() without an active subscription does not silently return; raises KalshiSubscriptionError (or whichever error type matches the WS error hierarchy).
- Test in
tests/ws/test_client.py covering the callback-only-no-subscribe path.
- README updated to clarify the subscribe-then-callback flow.
- Sibling foot-gun "
run_forever graceful shutdown" tracked in a separate issue.
From #106 F-P-16. Severity: medium UX foot-gun (silent no-op).
Behavior
kalshi/ws/client.pyonly starts_recv_loopfrom inside_subscribe_via_lockat line 378 (self._ensure_recv_loop()). If a user:KalshiWebSocket(...),ws.on("trade")(handler)/ws.on_error(handler),async with ws.connect() as session:,await session.run_forever(),…without ever calling a
subscribe_*method,run_forever()returns immediately (becauseself._recv_taskisNone, seekalshi/ws/client.py:529-532). No messages are ever read from the socket. Server-pushed control frames are also silently dropped.This is the documented callback-mode workflow in
README.md. The README does not say a subscribe is also required, and the type system doesn't catch it.Options
A. Auto-start
_recv_loopon connect. Simple, drains server pushes, but no SID mapping exists so data frames still no-op until subscribe. Doesn't fix the actual signal-loss problem, just the silent-return symptom.B. Raise in
run_forever()if no subscription exists. Loud signal at the call site, no silent hang. Could be aKalshiSubscriptionError("run_forever() requires at least one active subscription").C. Document only. Update README + add a docstring on
run_forevercalling out the requirement.Recommendation
B, with the option to soften to a warning later if it proves too strict. Errors at the obvious failure point beat silent no-ops.
Acceptance
run_forever()without an active subscription does not silently return; raisesKalshiSubscriptionError(or whichever error type matches the WS error hierarchy).tests/ws/test_client.pycovering the callback-only-no-subscribe path.run_forevergraceful shutdown" tracked in a separate issue.