Skip to content

_wait_for_response leaks TimeoutError instead of KalshiSubscriptionError #314

Description

@TexasCoding

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingwsWebSocket-related

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions