Summary
KalshiSequenceGapError and KalshiSubscriptionError lack the programmatic context a caller needs to write a recovery handler.
KalshiSequenceGapError
Bare subclass with only a docstring — no channel, sid, last_seq, next_seq attributes. Per docs/websockets.md:196-198 and docs/errors.md:124-127 the class exists "for callers wiring their own resync handlers" on top of subscribe(channel, ...). A caller catching it gets only str(e) — they can't tell which channel/sid is gapped or how far behind they are without parsing the message string.
Compare to KalshiRateLimitError (exposes .retry_after) — the asymmetry is the gap.
KalshiSubscriptionError
Exposes only error_code and the message string. A caller catching it cannot programmatically tell which channel or subscription failed, which matters when several subscribe_* calls land in parallel.
Location
kalshi/errors.py:76-89
Evidence
# kalshi/errors.py:76-77
class KalshiSequenceGapError(KalshiWebSocketError):
"""Sequence gap detected that could not be resolved via resync."""
# kalshi/errors.py:84-89
class KalshiSubscriptionError(KalshiWebSocketError):
"""Subscribe/unsubscribe request rejected by server."""
def __init__(self, message: str, error_code: int | None = None) -> None:
self.error_code = error_code
super().__init__(message)
Recommended fix
Extend both classes with structured context populated at every raise site.
class KalshiSequenceGapError(KalshiWebSocketError):
def __init__(
self,
message: str,
*,
channel: str | None = None,
sid: int | None = None,
client_id: int | None = None,
last_seq: int | None = None,
next_seq: int | None = None,
) -> None:
self.channel = channel
self.sid = sid
self.client_id = client_id
self.last_seq = last_seq
self.next_seq = next_seq
super().__init__(message)
class KalshiSubscriptionError(KalshiWebSocketError):
def __init__(
self,
message: str,
*,
error_code: int | None = None,
channel: str | None = None,
client_id: int | None = None,
op: Literal["subscribe", "unsubscribe", "update_subscription"] | None = None,
) -> None:
self.error_code = error_code
self.channel = channel
self.client_id = client_id
self.op = op
super().__init__(message)
Update every raise site in kalshi/ws/ to populate the new fields. Update docs/errors.md so the "exposed for custom resync" framing is actually usable.
Backward-compatible: positional message and keyword error_code preserved.
Severity & category
medium / dx, ws
Summary
KalshiSequenceGapErrorandKalshiSubscriptionErrorlack the programmatic context a caller needs to write a recovery handler.KalshiSequenceGapError
Bare subclass with only a docstring — no
channel,sid,last_seq,next_seqattributes. Perdocs/websockets.md:196-198anddocs/errors.md:124-127the class exists "for callers wiring their own resync handlers" on top ofsubscribe(channel, ...). A caller catching it gets onlystr(e)— they can't tell which channel/sid is gapped or how far behind they are without parsing the message string.Compare to
KalshiRateLimitError(exposes.retry_after) — the asymmetry is the gap.KalshiSubscriptionError
Exposes only
error_codeand the message string. A caller catching it cannot programmatically tell which channel or subscription failed, which matters when severalsubscribe_*calls land in parallel.Location
kalshi/errors.py:76-89Evidence
Recommended fix
Extend both classes with structured context populated at every raise site.
Update every raise site in
kalshi/ws/to populate the new fields. Updatedocs/errors.mdso the "exposed for custom resync" framing is actually usable.Backward-compatible: positional
messageand keyworderror_codepreserved.Severity & category
medium / dx, ws