Repository navigation
Description
Summary
The transport collapses two semantically distinct timeout cases into a single bare KalshiError(f"Request timed out: ..."):
(1) httpx.PoolTimeout (the request never left the client)
httpx.PoolTimeout is a subclass of httpx.TimeoutException, so it's caught by the same handler as connect/read/write timeouts. The request never reached the wire — it's always safe to retry regardless of HTTP method. Currently:
- GET/HEAD/OPTIONS retry (correct).
- POST/DELETE raise
KalshiError("Request timed out: ...")without retrying — even though there's no idempotency risk because the request never reached Kalshi's servers.
(2) Real timeout on POST (the request may have committed)
For orders.create(), this is the most operationally important error to handle. The user needs to know whether to retry with the same client_order_id (Kalshi dedups), query order status, or assume no-op. Today the SDK collapses it into bare KalshiError, so callers cannot write except KalshiTimeoutError to drive the recovery path.
Location
kalshi/_base_client.py:151-165(sync)kalshi/_base_client.py:269-282(async)
Evidence
# kalshi/_base_client.py:152-163
except httpx.TimeoutException as e:
...
last_error = KalshiError(f"Request timed out: {method.upper()} {path}")
if method.upper() in RETRYABLE_METHODS and attempt < self._config.max_retries:
...
raise last_error from e # POST falls through as generic KalshiErrorRecommended fix
Introduce two exception types:
class KalshiTimeoutError(KalshiError):
"""Request timed out. The server may or may not have processed it."""
class KalshiPoolExhaustedError(KalshiError):
"""Connection pool exhausted; request never reached the wire. Safe to retry."""In both transports, add a dedicated except httpx.PoolTimeout arm BEFORE the generic httpx.TimeoutException handler:
except httpx.PoolTimeout as e:
last_error = KalshiPoolExhaustedError(
f"Connection pool exhausted on {method} {path}. "
f"Raise KalshiConfig.limits.max_connections."
)
# Safe to retry on any verb — request never reached the wire
if attempt < self._config.max_retries:
delay = _compute_backoff(attempt, self._config)
time.sleep(delay)
continue
raise last_error from e
except httpx.TimeoutException as e:
last_error = KalshiTimeoutError(f"Request timed out: {method} {path}")
...Document the client_order_id recovery pattern in docs/resources/orders.md:
On
KalshiTimeoutErrorafterPOST /portfolio/orders, query
GET /portfolio/orders?client_order_id=...— your request may have
succeeded server-side.
Tests:
test_pool_timeout_retried_on_posttest_pool_timeout_raises_KalshiPoolExhaustedErrortest_read_timeout_on_post_raises_KalshiTimeoutError_no_retry
Severity & category
medium / correctness, dx
Activity
feat(errors): add Conflict/Timeout/PoolExhausted; structure WS error …