You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
sign_request strips query string but not URL fragment, producing 401 on '#'-bearing paths #317
KalshiAuth.sign_request strips the query string before signing but leaves any #fragment intact. Per RFC 3986 §3.5, fragments are client-side; httpx (like every conforming HTTP client) drops them from the request-target before transmission. The signer therefore signs /trade-api/v2/markets/EVT#tab=foo while httpx sends /trade-api/v2/markets/EVT — a guaranteed signature mismatch, surfacing as a 401 with KalshiAuthError and no hint that a stray # is the cause.
Realistic triggers: a caller templating a path with a UI deep-link, an #anchor inherited from a copy-paste, or any custom path-builder that doesn't sanitize fragments.
Evidence
kalshi/auth.py:262-273:
iftimestamp_msisNone:
timestamp_ms=int(time.time() *1000)
# Strip query parameters before signingclean_path=path.split("?")[0]
# Strip trailing slash for canonical formifclean_path!="/"andclean_path.endswith("/"):
clean_path=clean_path.rstrip("/")
# Normalize percent-encoding to uppercase hex (RFC 3986 section 2.1)clean_path=_normalize_percent_encoding(clean_path)
Only ? is stripped; # survives into the signed message.
kalshi/_base_client.py (_map_error) routes 401 to KalshiAuthError with no indication that the fragment caused it, and 401 is correctly absent from RETRYABLE_STATUS_CODES.
Mirror the strip in any wire-path canonicalization upstream so signing and httpx see byte-identical bytes. Add a regression test asserting sign_request("GET", "/trade-api/v2/markets/EVT#tab=foo") produces the same signature as sign_request("GET", "/trade-api/v2/markets/EVT").
Activity