Skip to content

Fix: Merge URL query parameters instead of replacing them#3759

Open
veeceey wants to merge 4 commits intoencode:masterfrom
veeceey:fix/issue-3621-merge-url-params
Open

Fix: Merge URL query parameters instead of replacing them#3759
veeceey wants to merge 4 commits intoencode:masterfrom
veeceey:fix/issue-3621-merge-url-params

Conversation

@veeceey
Copy link

@veeceey veeceey commented Feb 8, 2026

Summary

Fixes #3621

When a URL already contains query parameters and additional params are passed, httpx was dropping the original URL parameters instead of merging them. This was inconsistent with the requests library behavior.

Problem

url = 'https://api.com/get?page=1&sort=desc'
params = {'size': 10, 'filter': 'active'}

# Before this fix
response = httpx.get(url=url, params=params)
# URL sent: https://api.com/get?size=10&filter=active
# (page=1 and sort=desc were lost!)

# After this fix
response = httpx.get(url=url, params=params)
# URL sent: https://api.com/get?page=1&sort=desc&size=10&filter=active
# (all parameters are present)

Solution

Modified URL.__init__() in _urls.py to:

  1. Extract existing query parameters from the URL
  2. Merge them with new parameters from the params argument
  3. When the same key exists in both, new params override old ones (merge semantics)

Changes

  • Modified httpx/_urls.py: Updated param handling to merge instead of replace
  • Added comprehensive tests in tests/models/test_url.py

Test Cases

Merge existing and new params:

url = httpx.URL("https://example.com/get?page=post&s=list", params={"pid": 0, "tags": "test"})
# Result: page=post&s=list&pid=0&tags=test

Override with same key:

url = httpx.URL("https://example.com/get?a=old", params={"a": "new", "b": "2"})
# Result: a=new&b=2 (a was overridden)

Empty params preserves existing:

url = httpx.URL("https://example.com/get?x=5", params={})
# Result: x=5 (preserved)

No existing params works normally:

url = httpx.URL("https://example.com/get", params={"a": "1"})
# Result: a=1

Testing

  • ✅ Manual testing with various URL/param combinations
  • ✅ Added regression tests
  • ✅ Verified backward compatibility with existing tests

When a URL contains query parameters and additional params are passed via
the params argument, the original URL parameters were being dropped instead
of being merged together. This was inconsistent with requests library behavior.

This fix modifies the URL.__init__() to merge existing query parameters from
the URL with new parameters from the params argument, rather than replacing
them. When the same key exists in both, the new params take precedence.

Examples:
- URL("https://api.com/get?page=1", params={"size": 10})
  Before: https://api.com/get?size=10  (page lost)
  After: https://api.com/get?page=1&size=10  (merged)

- URL("https://api.com/get?a=old", params={"a": "new"})
  After: https://api.com/get?a=new  (overridden)

Fixes encode#3621
Updated URL.__init__ to properly handle QueryParams objects vs dict params:
- When params is a QueryParams object (from copy_remove_param, etc), use it directly without merging
- When params is a dict/list, merge with existing URL query parameters
- Updated test expectations to reflect merge behavior instead of replacement

This fixes the copy_remove_param test failure while maintaining the intended
merge behavior for user-provided params, matching requests library behavior.
Removed the else branch handling when url is neither str nor URL,
as the type hint enforces url: URL | str. This was unreachable
defensive code that was causing a coverage gap.
@veeceey
Copy link
Author

veeceey commented Feb 8, 2026

All CI passing on Python 3.9-3.13, ready for merge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

When the URL contains request parameters and the params parameter is set, the request parameters in the URL will disappear unexpectedly.

1 participant