Repository navigation
Description
Context
Surfaced by the multi-LLM review of PR #403 (perps/margin API branch). PerpsConfig already declares separate _PERPS_REST_HOSTS / _PERPS_WS_HOSTS frozensets, but the host allowlist check and the path checks do not actually use that split, so a REST URL on a WS host (or vice versa) is accepted and ws_base_url is never path-validated.
Problem
In kalshi/perps/config.py:
_PERPS_KNOWN_HOSTS = _PERPS_REST_HOSTS | _PERPS_WS_HOSTS(line 39) is the union of both host sets._validate_perps_urlis the single helper used for both fields and checkshost not in _PERPS_KNOWN_HOSTS(line 172). It is called forbase_url(lines 84-90) andws_base_url(lines 91-97) with the same union. The_PERPS_REST_HOSTS/_PERPS_WS_HOSTSsplit (lines 33-38) is therefore dead for per-field host validation — only the prod/demo-pair guard (lines 108-120) consumes the individual host constants.- The
/trade-api/v2path check (lines 100-105) is applied only tobase_url. There is no equivalent check enforcing the perps WS path (/trade-api/ws/v2/margin) onws_base_url.
Because the perps REST and WS hosts genuinely differ (external-api.kalshi.com vs external-api-margin-ws.kalshi.com) — unlike the prediction API where one host serves both — the union allowlist lets a caller cross the wires without the guard firing. Verified empirically against current code:
# REST base_url pointed at the WS host — accepted (passes union host check + /trade-api/v2 path):
PerpsConfig(base_url='https://external-api-margin-ws.kalshi.com/trade-api/v2', ws_base_url=<prod ws>)
-> ACCEPTED
# ws_base_url pointed at the REST host with an arbitrary path — accepted (union host check, no WS path check):
PerpsConfig(base_url=<prod rest>, ws_base_url='wss://external-api.kalshi.com/anything/here')
-> ACCEPTED
# ws_base_url on the correct WS host but a wrong path — accepted (no WS path check at all):
PerpsConfig(base_url=<prod rest>, ws_base_url='wss://external-api-margin-ws.kalshi.com/totally/wrong/path')
-> ACCEPTED
Concrete failure: a typo or copy-paste that swaps the REST and WS endpoints (e.g. a REST base_url aimed at the margin-WS host, or a WS ws_base_url aimed at the REST host) passes construction silently and only fails later at connect/request time with an opaque transport error, instead of being caught at config construction where every other perps URL footgun is rejected. The host allowlist is a security guard (it gates where signed, API-key-bearing requests are sent); allowing the wrong perps host through it weakens that guard for the perps surface.
Proposed fix
Minimal, surgical change in kalshi/perps/config.py:
- Pass the field-specific host set into
_validate_perps_urlinstead of the union. Add aknown_hosts: frozenset[str]parameter and call it with_PERPS_REST_HOSTSforbase_urland_PERPS_WS_HOSTSforws_base_url. Update the error message / log to reference the passed set. (The union_PERPS_KNOWN_HOSTScan then be dropped, or kept only if referenced elsewhere.) - Add a
ws_base_urlpath check mirroring the REST one: after trailing-slash stripping, asserturlparse(self.ws_base_url).path == "/trade-api/ws/v2/margin"and raise aValueErrornaming the field, the expected path, and the actualws_base_urlwhen it does not match. Keep theallow_unknown_hostescape hatch governing only the host check, consistent with the REST side.
Acceptance criteria
-
base_urlis validated against_PERPS_REST_HOSTSonly; a perps WS host (external-api-margin-ws.kalshi.com/.demo.kalshi.co) asbase_urlraisesValueErrorunlessallow_unknown_hostis set. -
ws_base_urlis validated against_PERPS_WS_HOSTSonly; a perps REST host (external-api.kalshi.com/.demo.kalshi.co) asws_base_urlraisesValueErrorunlessallow_unknown_hostis set. -
ws_base_urlmust have path/trade-api/ws/v2/margin; a valid WS host with a wrong path raisesValueError. -
PerpsConfig.production()/PerpsConfig.demo()and the existinglocalhostcase (ws://localhost/trade-api/ws/v2/margin) still construct successfully — note localhost is gated by_LOCAL_HOSTS, so confirm the new WS path check still allows it (the canonical localhost WS URL already uses the expected path). - Regression tests in
tests/perps/test_config.py:-
base_urlon a WS host is rejected. -
ws_base_urlon a REST host is rejected. -
ws_base_urlwith a wrong path on a valid WS host is rejected. - existing factory / trailing-slash / split-environment / localhost tests still pass.
-
Notes
- Deferred from PR perps: full Perps (margin) API — REST + WebSocket + SCM/Klear (#387) #403: the headline review findings were fixed in f8cc960 (ws), da953b0 (klear), f2ef973 (rest/models); this host-split/path-check item was carried over and is not addressed by those commits (verified against current
kalshi/perps/config.py). - Tradeoff / behavior change: this makes previously-accepted (mis)configurations raise at construction. That is the intended hardening, but it is technically stricter — it is not a public output/format change, so it is not labeled
breaking; the only configs it newly rejects are ones that were already misconfigured. - The prediction-API
KalshiConfig(kalshi/config.py:27) uses a single shared_KNOWN_HOSTSbecause one host serves both REST and WS there, and it likewise has no dedicated WS path check (onlybase_urlchecks/trade-api/v2at lines 112-115). Tightening the WS path on the prediction side is a separate, optional consideration — out of scope here; this issue is perps-specific because the perps hosts actually diverge. - Related: perps EPIC perps: [EPIC] Perps (margin) API — full SDK implementation tracker #387.
Activity
perps(ws): address PR #403 bot review — model-built orphan-unsubscrib…