Skip to content

perps: PerpsConfig validates base_url/ws_base_url against the union of REST+WS hosts and skips the WS path check #412

Description

@TexasCoding

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_url is the single helper used for both fields and checks host not in _PERPS_KNOWN_HOSTS (line 172). It is called for base_url (lines 84-90) and ws_base_url (lines 91-97) with the same union. The _PERPS_REST_HOSTS / _PERPS_WS_HOSTS split (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/v2 path check (lines 100-105) is applied only to base_url. There is no equivalent check enforcing the perps WS path (/trade-api/ws/v2/margin) on ws_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:

  1. Pass the field-specific host set into _validate_perps_url instead of the union. Add a known_hosts: frozenset[str] parameter and call it with _PERPS_REST_HOSTS for base_url and _PERPS_WS_HOSTS for ws_base_url. Update the error message / log to reference the passed set. (The union _PERPS_KNOWN_HOSTS can then be dropped, or kept only if referenced elsewhere.)
  2. Add a ws_base_url path check mirroring the REST one: after trailing-slash stripping, assert urlparse(self.ws_base_url).path == "/trade-api/ws/v2/margin" and raise a ValueError naming the field, the expected path, and the actual ws_base_url when it does not match. Keep the allow_unknown_host escape hatch governing only the host check, consistent with the REST side.

Acceptance criteria

  • base_url is validated against _PERPS_REST_HOSTS only; a perps WS host (external-api-margin-ws.kalshi.com / .demo.kalshi.co) as base_url raises ValueError unless allow_unknown_host is set.
  • ws_base_url is validated against _PERPS_WS_HOSTS only; a perps REST host (external-api.kalshi.com / .demo.kalshi.co) as ws_base_url raises ValueError unless allow_unknown_host is set.
  • ws_base_url must have path /trade-api/ws/v2/margin; a valid WS host with a wrong path raises ValueError.
  • PerpsConfig.production() / PerpsConfig.demo() and the existing localhost case (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_url on a WS host is rejected.
    • ws_base_url on a REST host is rejected.
    • ws_base_url with 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_HOSTS because one host serves both REST and WS there, and it likewise has no dedicated WS path check (only base_url checks /trade-api/v2 at 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    perpsPerps / margin (perpetual futures) APIsecuritySecurity-related concern

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions