Summary
When a request passes its own params, the client-level params are silently discarded instead of merged. Since Client(params={"api_key": ...}) is the standard way to configure an API client, any request that adds a query parameter silently drops the credential.
A related defect: per-request cookies are emitted as a second Cookie: header rather than merged into one.
Cause
src/lib.rs:416 (and the identical line in _stream, src/lib.rs:589):
let params = params.or_else(|| self.params.clone());
or_else replaces; it never merges. For cookies, src/lib.rs:460 appends a fresh COOKIE header via request_builder.header(COOKIE, ...) on top of the client's existing one.
Reproduction
Against a local server echoing the raw request line and headers:
c = httpr.Client(params={"api_key": "SECRET"}, cookies={"session": "abc"})
c.get(url) # baseline
c.get(url, params={"q": "1"}, cookies={"tracking": "t1"})
|
httpr |
httpx |
| client only |
GET /x?api_key=SECRET |
GET /x?api_key=SECRET |
| + per-request |
GET /x?q=1 ❌ |
GET /x?api_key=SECRET&q=1 |
| cookies |
cookie: session=abc and cookie: tracking=t1 (two headers) ❌ |
Cookie: session=abc; tracking=t1 (one) |
RFC 6265 §5.4 states a user agent MUST NOT attach more than one Cookie header field; servers commonly read only the first, so tracking is silently ignored.
Expected
Per-request params and cookies merge with the client-level values, with the per-request value winning on key collision — matching httpx and requests.
Proposed fix
In both request() and _stream() in src/lib.rs:
- Build the effective params by cloning
self.params and then extending with the per-request map (per-request key wins), instead of or_else.
- Merge per-request cookies into the existing
COOKIE header value rather than adding a second header.
Note the two functions duplicate this whole block; factoring the shared setup into one helper would prevent the two copies drifting.
Suggested tests
- Client-level + per-request params both appear in the query string.
- Per-request value overrides the client-level value for the same key.
- Exactly one
Cookie header is sent, containing both cookies.
Size
~1 hour.
Summary
When a request passes its own
params, the client-levelparamsare silently discarded instead of merged. SinceClient(params={"api_key": ...})is the standard way to configure an API client, any request that adds a query parameter silently drops the credential.A related defect: per-request
cookiesare emitted as a secondCookie:header rather than merged into one.Cause
src/lib.rs:416(and the identical line in_stream,src/lib.rs:589):or_elsereplaces; it never merges. For cookies,src/lib.rs:460appends a freshCOOKIEheader viarequest_builder.header(COOKIE, ...)on top of the client's existing one.Reproduction
Against a local server echoing the raw request line and headers:
GET /x?api_key=SECRETGET /x?api_key=SECRETGET /x?q=1❌GET /x?api_key=SECRET&q=1cookie: session=abcandcookie: tracking=t1(two headers) ❌Cookie: session=abc; tracking=t1(one)RFC 6265 §5.4 states a user agent MUST NOT attach more than one
Cookieheader field; servers commonly read only the first, sotrackingis silently ignored.Expected
Per-request
paramsandcookiesmerge with the client-level values, with the per-request value winning on key collision — matchinghttpxandrequests.Proposed fix
In both
request()and_stream()insrc/lib.rs:self.paramsand then extending with the per-request map (per-request key wins), instead ofor_else.COOKIEheader value rather than adding a second header.Note the two functions duplicate this whole block; factoring the shared setup into one helper would prevent the two copies drifting.
Suggested tests
Cookieheader is sent, containing both cookies.Size
~1 hour.