Repository navigation
feat(transport): widen retry codes; typed timeouts; bounded errors; base_url validation; close ownership - #228
Conversation
…ase_url validation; close ownership - Retry: widen RETRYABLE_STATUS_CODES to include 408/425/520-524 (Cloudflare). POST/DELETE still never retry. - KalshiConfig.total_timeout: wall-clock deadline (None = legacy unbounded). - _map_error: 422 -> KalshiValidationError, 409 -> KalshiConflictError. Body capped at 16KB via Content-Length; message truncated to 1024 chars. - httpx.PoolTimeout -> KalshiPoolExhaustedError (always retryable, even POST); httpx.TimeoutException -> KalshiTimeoutError. - KalshiConfig.__post_init__ validates base_url path == /trade-api/v2 (raises ValueError otherwise). - KalshiClient/AsyncKalshiClient.close() only shuts down KalshiAuth when client owns it (auth was constructed locally, not passed via auth= kwarg). Closes #192, Closes #193, Closes #201, Closes #202, Closes #203, Closes #204, Closes #210 Refs #224
Code Review — Wave 1-A Transport HardeningOverall: solid, well-motivated PR with good test coverage across all 7 issues. A few things worth addressing before merge. Bugs / Correctness1. The inline comment says "short-circuit before httpx materialises it into memory", but # #203: skip JSON parsing and bound the error message for large error bodies.
# Note: httpx has already read the body; this prevents spending CPU on
# parsing and caps log/Sentry payload size.If actual memory protection is desired, the transport would need to switch to streaming responses with a read limit — that's a bigger refactor, not required now, but worth a follow-up issue. 2. # __init__
self._auth_owned: bool = auth is None # True when WE build it
# from_env override
client._auth_owned = auth is not None # True when try_from_env() returned authThese two lines use opposite boolean expressions to set the same flag, which is correct today but easy to break on a future refactor. The disconnect is that # __init__
self._auth_owned: bool = kwargs.pop("_auth_owned", auth is None)Or just document the inversion explicitly with a 3.
if self.total_timeout is not None and self.total_timeout <= 0:
raise ValueError(f"total_timeout must be positive, got {self.total_timeout}")Design / Approach4. The 5. Pool timeout In the budget-check path, the pool timeout raises Test Coverage Gaps6. No test for
7. No test that 8. 425 (Too Early) is in 408 and 521 have explicit test cases. 425 is a niche TLS early-data code — even a one-line unit test against Nits
Summary
Blocking: none of these are hard blockers, but items 3 (negative |
Wave 1-A of the audit umbrella. 7 issues; HTTP transport hardening.
Changes
RETRYABLE_STATUS_CODESto include 408/425/520–524 (Cloudflare 5xx). POST/DELETE still never retry.KalshiConfig.total_timeout: wall-clock deadline (None = legacy unbounded)._map_error: 422 →KalshiValidationError, 409 →KalshiConflictError. Body capped at 16KB via Content-Length; message truncated to 1024 chars.KalshiConfig.__post_init__validates base_url path ==/trade-api/v2._map_errorbody buffering bounded (memory + log volume).httpx.PoolTimeout→KalshiPoolExhaustedError(always retryable, even POST).httpx.TimeoutException→KalshiTimeoutError.KalshiClient/AsyncKalshiClient.close()only shut downKalshiAuthwhen client owns it (_auth_owned).Tests
15 new test classes / suites; 183 tests pass; ruff + mypy --strict clean on 7 changed files.
Closes #192, Closes #193, Closes #201, Closes #202, Closes #203, Closes #204, Closes #210
Refs #224