Skip to content

config.extra_headers attached to httpx.Client AND re-merged per request #341

Closed

Description

@TexasCoding

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 passes config.extra_headers as 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 includes config.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_merge mirrors sync.

Suggested fix

Pick one layer:

  • Preferred: drop "headers": config.extra_headers from the httpx.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_merge the single source of truth and makes the documented header ordering in SyncTransport.request actually authoritative.
  • Alternative: keep headers=config.extra_headers on the client and drop self._config.extra_headers from the _ci_merge call so per-call extra_headers correctly 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).

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

    performancePerformance / hot-path concernpolishCode-quality and DX improvements; non-functional

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions