Skip to content

Multi-value and boolean query params are corrupted by str() coercion #87

Description

@thomasht86

Summary

Query parameters are coerced with str(v), so any non-string value is serialised using Python's repr rules rather than URL conventions. Lists become a URL-encoded Python literal instead of repeated keys, and booleans are sent capitalised. Form data with list values fails outright with an opaque error.

Cause

httpr/__init__.py:357 (repeated at lines 595, 744 and 916 for stream, async request and async stream):

if "params" in kwargs and kwargs["params"] is not None:
    kwargs["params"] = {k: str(v) for k, v in kwargs["params"].items()}

str(["a", "b"]) is "['a', 'b']", and str(True) is "True". The Rust side receives IndexMap<String, String>, so a single scalar per key is the only representable shape.

For data, src/lib.rs:475 calls request_builder.form(&form_data) on a serde_json::Value; serde_urlencoded cannot serialise a sequence value and reqwest stores the error until send.

Reproduction

Against a local server echoing the request line:

c = httpr.Client()
c.get(url, params={"tag": ["a", "b"]})
c.get(url, params={"n": 5, "flag": True})
c.post(url, data={"tag": ["a", "b"]})
call httpr httpx
params={"tag": ["a","b"]} GET /x?tag=%5B%27a%27%2C+%27b%27%5D GET /x?tag=a&tag=b
params={"n": 5, "flag": True} GET /x?n=5&flag=True GET /x?n=5&flag=true
data={"tag": ["a","b"]} RequestError: builder error sends tag=a&tag=b

The first row decodes to the literal string ['a', 'b'] as the value of tag.

Impact

Repeated query keys are the standard encoding for list-valued filters (?id=1&id=2, ?fields=a&fields=b), and lowercase true/false is what JSON-oriented APIs expect — flag=True is commonly parsed as a non-empty string, i.e. truthy regardless of intent. Both failures are silent. The data case at least errors, but builder error gives no indication of which parameter is at fault.

Expected

Match httpx: sequence values expand to repeated keys, True/False serialise as true/false, numbers serialise normally.

Proposed fix

Two parts:

  1. Python (httpr/__init__.py): replace the {k: str(v)} comprehension with a helper that flattens to a list of (key, value) pairs — expanding list/tuple values into repeated keys and mapping bool to "true"/"false" before falling back to str(). Apply it at all four call sites (they're currently copy-pasted; a shared _normalize_params() would fix that too).
  2. Rust (src/lib.rs): accept Vec<(String, String)> for params instead of IndexMapSSR. reqwest's .query() already accepts a slice of pairs and emits repeated keys, so the request-building code barely changes.

Update params in httpr/httpr.pyi (lines 18, 360, 399-401, 451, 552) to a type allowing sequence and scalar values.

Form data with list values can be handled the same way, or — if that's more scope than wanted — at minimum turned into a clear error naming the offending key rather than builder error.

Suggested tests

  • params={"tag": ["a","b"]} produces ?tag=a&tag=b.
  • params={"flag": True} produces ?flag=true.
  • params={"n": 5} produces ?n=5.
  • Same coverage via AsyncClient and via stream(), since the coercion is duplicated in all four places.

Size

~1-2 hours.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions