Repository navigation
Description
Problem
Both SyncTransport and AsyncTransport pass config.extra_headers as the httpx.Client default headers AND merge them into every per-request merged_headers via _ci_merge. The final wire form is correct (httpx's own merge resolves duplicates), but every user-supplied default header is dict-walked, lowercase-keyed, and shipped twice through httpx's header serializer on every request — measurable on the bench_request_hot_path target.
More importantly, the duplication blurs the documented header-precedence contract. SyncTransport.request's docstring promises config.extra_headers < per-request extra_headers < signed auth; the implementation truly resolves the order via _ci_merge, but the dangling "headers": config.extra_headers on the httpx.Client makes the precedence depend on httpx's own merging instead of the SDK's _ci_merge.
Evidence
kalshi/_base_client.py:220-229— sync client passesconfig.extra_headersas default headers:
client_kwargs: dict[str, Any] = {
"base_url": config.base_url,
"timeout": config.timeout,
"headers": config.extra_headers,
...
}
self._client = httpx.Client(**client_kwargs)kalshi/_base_client.py:280-284— per-request merge ALSO includesconfig.extra_headers:
base_headers: dict[str, str] = _ci_merge(
self._config.extra_headers,
extra_headers,
headers,
)kalshi/_base_client.py:443-452— async path mirrors the same shape.kalshi/_base_client.py:498-502— async per-request_ci_mergemirrors sync.
Suggested fix
Pick one layer:
- Preferred: drop
"headers": config.extra_headersfrom thehttpx.Client(...)kwargs in both transports._ci_merge(self._config.extra_headers, extra_headers, headers)already covers config defaults, so the wire output is identical. This keeps_ci_mergethe single source of truth and makes the documented header ordering inSyncTransport.requestactually authoritative. - Alternative: keep
headers=config.extra_headerson the client and dropself._config.extra_headersfrom the_ci_mergecall so per-callextra_headerscorrectly stack on top via httpx's own merge.
Whichever is chosen, add a unit test that exercises config.extra_headers + per-call extra_headers precedence end-to-end against a recording transport.
Source
Round-3 independent audit (reviewer: http_transport).
Activity
fix(security): KalshiConfig.extra_headers immutable post-construction…