Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions httpx/_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,43 @@ def __init__(self, url: URL | str = "", **kwargs: typing.Any) -> None:
kwargs[key] = value.decode("ascii")

if "params" in kwargs:
# Replace any "params" keyword with the raw "query" instead.
# Merge any "params" keyword with existing query params.
#
# When a URL already has query parameters and additional params
# are provided, we merge them together rather than replacing.
# This matches the behavior of the requests library.
#
# Ensure that empty params use `kwargs["query"] = None` rather
# than `kwargs["query"] = ""`, so that generated URLs do not
# include an empty trailing "?".
params = kwargs.pop("params")
kwargs["query"] = None if not params else str(QueryParams(params))

# If params is a QueryParams object, use it directly
# (for copy_* methods like copy_remove_param)
if isinstance(params, QueryParams):
kwargs["query"] = str(params) if params else None
else:
# Get existing query params from the URL
if isinstance(url, str):
parsed_url = urlparse(url)
existing_params = (
QueryParams(parsed_url.query)
if parsed_url.query
else QueryParams()
)
else: # isinstance(url, URL)
existing_params = url.params

# Merge existing and new params
if params:
merged_params = existing_params.merge(params)
kwargs["query"] = str(merged_params) if merged_params else None
elif existing_params:
# params is empty but URL has existing params - keep them
kwargs["query"] = str(existing_params)
else:
# Both empty - no query string
kwargs["query"] = None

if isinstance(url, str):
self._uri_reference = urlparse(url, **kwargs)
Expand Down
4 changes: 2 additions & 2 deletions tests/models/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def test_request_params():
request = httpx.Request(
"GET", "http://example.com?c=3", params={"a": "1", "b": "2"}
)
assert str(request.url) == "http://example.com?a=1&b=2"
assert str(request.url) == "http://example.com?c=3&a=1&b=2"

request = httpx.Request("GET", "http://example.com?a=1", params={})
assert str(request.url) == "http://example.com"
assert str(request.url) == "http://example.com?a=1"
66 changes: 64 additions & 2 deletions tests/models/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ def test_url_params():
url = httpx.URL(
"https://example.org:123/path/to/somewhere?b=456", params={"a": "123"}
)
assert str(url) == "https://example.org:123/path/to/somewhere?a=123"
assert url.params == httpx.QueryParams({"a": "123"})
assert str(url) == "https://example.org:123/path/to/somewhere?b=456&a=123"
assert url.params == httpx.QueryParams({"b": "456", "a": "123"})


# Tests for username and password
Expand Down Expand Up @@ -861,3 +861,65 @@ def test_ipv6_url_copy_with_host(url_str, new_host):
assert url.host == "::ffff:192.168.0.1"
assert url.netloc == b"[::ffff:192.168.0.1]:1234"
assert str(url) == "http://[::ffff:192.168.0.1]:1234"


def test_url_params_merge_with_existing_query():
"""
Test that when constructing a URL with both existing query params
and a params argument, they are merged together.

Regression test for issue #3621.
"""
# URL with existing query params + additional params argument
url = httpx.URL(
"https://example.com/get?page=post&s=list", params={"pid": 0, "tags": "test"}
)

assert url.path == "/get"
assert "page=post" in str(url)
assert "s=list" in str(url)
assert "pid=0" in str(url)
assert "tags=test" in str(url)

# Verify all params are present
params = dict(url.params)
assert params == {"page": "post", "s": "list", "pid": "0", "tags": "test"}


def test_url_params_override_with_same_key():
"""
Test that when a URL has existing query params and new params with
the same key are provided, the new params override the old ones.
"""
url = httpx.URL(
"https://example.com/get?a=old&b=keep", params={"a": "new", "c": "add"}
)

params = dict(url.params)
assert params["a"] == "new" # Overridden
assert params["b"] == "keep" # Preserved
assert params["c"] == "add" # Added


def test_url_params_empty_dict_preserves_existing():
"""
Test that passing an empty params dict doesn't remove existing URL params.
"""
url = httpx.URL("https://example.com/get?x=5&y=6", params={})

assert "x=5" in str(url)
assert "y=6" in str(url)
params = dict(url.params)
assert params == {"x": "5", "y": "6"}


def test_url_no_existing_params_with_params_arg():
"""
Test that a URL without existing query params works normally with params argument.
"""
url = httpx.URL("https://example.com/get", params={"a": "1", "b": "2"})

assert "a=1" in str(url)
assert "b=2" in str(url)
params = dict(url.params)
assert params == {"a": "1", "b": "2"}