Skip to content

Commit 5ba77da

Browse files
committed
Fix URL params to merge with existing query parameters instead of replacing them
When creating a URL with `URL(url, params=params)` or `Request(method, url, params=params)`, the params now merge with existing query parameters in the URL instead of completely replacing them. This makes the behavior consistent with the Python requests library. Before: URL("https://example.com?a=1", params={"b": "2"}) Result: "https://example.com?b=2" # 'a=1' was lost After: URL("https://example.com?a=1", params={"b": "2"}) Result: "https://example.com?a=1&b=2" # parameters merged Special cases handled: - Empty dict params={} preserves existing query parameters - None params preserves existing query parameters - QueryParams objects are used directly (for copy_* methods) - Overlapping parameter names are overridden by new values Fixes #652
1 parent 4fb9528 commit 5ba77da

3 files changed

Lines changed: 31 additions & 5 deletions

File tree

httpx/_urls.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,34 @@ def __init__(self, url: URL | str = "", **kwargs: typing.Any) -> None:
110110
# Ensure that empty params use `kwargs["query"] = None` rather
111111
# than `kwargs["query"] = ""`, so that generated URLs do not
112112
# include an empty trailing "?".
113+
#
114+
# Merge new params with existing query parameters instead of replacing them.
113115
params = kwargs.pop("params")
114-
kwargs["query"] = None if not params else str(QueryParams(params))
116+
# Get existing query parameters from the URL
117+
if isinstance(url, str):
118+
parsed_url = urlparse(url)
119+
existing_params = QueryParams(parsed_url.query)
120+
elif isinstance(url, URL):
121+
existing_params = url.params
122+
else:
123+
existing_params = QueryParams()
124+
125+
if isinstance(params, QueryParams):
126+
# If params is a QueryParams object, use it directly (for copy_* methods)
127+
kwargs["query"] = None if not params else str(params)
128+
elif params:
129+
# Merge existing parameters with new params (dict, list, etc.)
130+
merged_params = existing_params.merge(params)
131+
kwargs["query"] = None if not merged_params else str(merged_params)
132+
elif isinstance(params, dict) and not params:
133+
# If params is an empty dict, keep existing query parameters
134+
kwargs["query"] = None if not existing_params else str(existing_params)
135+
elif params is None:
136+
# If params is None, keep existing query parameters
137+
kwargs["query"] = None if not existing_params else str(existing_params)
138+
else:
139+
# Fallback case
140+
kwargs["query"] = None if not params else str(params)
115141

116142
if isinstance(url, str):
117143
self._uri_reference = urlparse(url, **kwargs)

tests/models/test_requests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def test_request_params():
235235
request = httpx.Request(
236236
"GET", "http://example.com?c=3", params={"a": "1", "b": "2"}
237237
)
238-
assert str(request.url) == "http://example.com?a=1&b=2"
238+
assert str(request.url) == "http://example.com?c=3&a=1&b=2"
239239

240240
request = httpx.Request("GET", "http://example.com?a=1", params={})
241-
assert str(request.url) == "http://example.com"
241+
assert str(request.url) == "http://example.com?a=1"

tests/models/test_url.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ def test_url_params():
159159
url = httpx.URL(
160160
"https://example.org:123/path/to/somewhere?b=456", params={"a": "123"}
161161
)
162-
assert str(url) == "https://example.org:123/path/to/somewhere?a=123"
163-
assert url.params == httpx.QueryParams({"a": "123"})
162+
assert str(url) == "https://example.org:123/path/to/somewhere?b=456&a=123"
163+
assert url.params == httpx.QueryParams({"b": "456", "a": "123"})
164164

165165

166166
# Tests for username and password

0 commit comments

Comments
 (0)