Problem
ChannelsManager._wait_for_response only catches ConnectionClosed. When asyncio.wait_for(..., timeout=remaining) exceeds its timeout, the built-in TimeoutError (which asyncio.TimeoutError aliases since 3.11) escapes to callers of subscribe() / unsubscribe(). The structured KalshiSubscriptionError path keyed on remaining <= 0 at the top of the loop is never reached because the next iteration never runs.
Consumers that branch on SDK exception types for error recovery (the #213 contract that gives callers channel / client_id / op on the error) lose that surface for the timeout case.
Evidence
kalshi/ws/channels.py:188-220:
deadline = asyncio.get_running_loop().time() + timeout
while True:
remaining = deadline - asyncio.get_running_loop().time()
if remaining <= 0:
raise KalshiSubscriptionError(
f"Timed out waiting for response to command {msg_id}",
channel=channel, client_id=client_id, op=op,
)
try:
raw = await asyncio.wait_for(
self._connection.recv(), timeout=remaining
)
except ConnectionClosed as e:
raise KalshiConnectionError(...) from e
Only ConnectionClosed is intercepted; TimeoutError propagates.
Suggested fix
Add a TimeoutError branch beside the ConnectionClosed handler that raises the same structured KalshiSubscriptionError the top-of-loop check would:
except TimeoutError:
raise KalshiSubscriptionError(
f"Timed out waiting for response to command {msg_id}",
channel=channel, client_id=client_id, op=op,
)
Keep the remaining <= 0 guard for defense in depth. Add a regression test that drives _wait_for_response against a _connection.recv mock that never returns and asserts KalshiSubscriptionError with the expected channel / client_id / op fields.
Source
Round-3 independent audit (reviewer: gemini).
Problem
ChannelsManager._wait_for_responseonly catchesConnectionClosed. Whenasyncio.wait_for(..., timeout=remaining)exceeds its timeout, the built-inTimeoutError(whichasyncio.TimeoutErroraliases since 3.11) escapes to callers ofsubscribe()/unsubscribe(). The structuredKalshiSubscriptionErrorpath keyed onremaining <= 0at the top of the loop is never reached because the next iteration never runs.Consumers that branch on SDK exception types for error recovery (the #213 contract that gives callers
channel/client_id/opon the error) lose that surface for the timeout case.Evidence
kalshi/ws/channels.py:188-220:Only
ConnectionClosedis intercepted;TimeoutErrorpropagates.Suggested fix
Add a
TimeoutErrorbranch beside theConnectionClosedhandler that raises the same structuredKalshiSubscriptionErrorthe top-of-loop check would:Keep the
remaining <= 0guard for defense in depth. Add a regression test that drives_wait_for_responseagainst a_connection.recvmock that never returns and assertsKalshiSubscriptionErrorwith the expectedchannel/client_id/opfields.Source
Round-3 independent audit (reviewer:
gemini).