Skip to content

Errors: add KalshiTimeoutError + KalshiPoolExhaustedError (typed timeout semantics) #204

Closed

Description

@TexasCoding

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 KalshiError

Recommended 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 KalshiTimeoutError after POST /portfolio/orders, query
GET /portfolio/orders?client_order_id=... — your request may have
succeeded server-side.

Tests:

  • test_pool_timeout_retried_on_post
  • test_pool_timeout_raises_KalshiPoolExhaustedError
  • test_read_timeout_on_post_raises_KalshiTimeoutError_no_retry

Severity & category

medium / correctness, dx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions