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
15 changes: 13 additions & 2 deletions src/httpx2/httpx2/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
ResponseExtensions,
SyncByteStream,
)
from ._urls import URL
from ._urls import URL, QueryParams
from ._utils import to_bytes_or_str, to_str

__all__ = ["Cookies", "Headers", "Request", "Response"]
Expand Down Expand Up @@ -385,7 +385,18 @@ def __init__(
extensions: RequestExtensions | None = None,
) -> None:
self.method = method.upper()
self.url = URL(url) if params is None else URL(url, params=params)
if params is None:
self.url = URL(url)
else:
base_url = URL(url)
params_obj = QueryParams(params)
if params_obj:
new_params_bytes = str(params_obj).encode("ascii")
existing_query = base_url.query # b"" if no query
merged_query = (existing_query + b"&" + new_params_bytes) if existing_query else new_params_bytes
self.url = base_url.copy_with(query=merged_query)
else:
self.url = base_url
self.headers = Headers(headers)
self.extensions = {} if extensions is None else dict(extensions)

Expand Down
27 changes: 27 additions & 0 deletions tests/httpx2/client/test_queryparams.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,30 @@ def test_client_queryparams_echo():

assert response.status_code == 200
assert response.url == "http://example.org/echo_queryparams?first=str&second=dict"


def test_base_url_with_request_params():
# Query params in the request URL must not be dropped when request-level
# params are also passed.
client = httpx2.Client(base_url="https://api.example.com/v1/")
request = client.build_request("GET", "users?active=true", params={"page": "2"})

assert str(request.url) == "https://api.example.com/v1/users?active=true&page=2"


def test_base_url_with_client_params_and_url_query():
# Client-level params must be appended to query params already present in
# the request URL, not replace them.
client = httpx2.Client(base_url="https://api.example.com/v1/", params={"api_key": "abc"})
request = client.build_request("GET", "users?active=true")

assert str(request.url) == "https://api.example.com/v1/users?active=true&api_key=abc"


def test_base_url_with_client_params_request_params_and_url_query():
# All three sources of query params (URL, client-level, request-level)
# must be combined without any being dropped.
client = httpx2.Client(base_url="https://api.example.com/v1/", params={"api_key": "abc"})
request = client.build_request("GET", "users?active=true", params={"page": "2"})

assert str(request.url) == "https://api.example.com/v1/users?active=true&api_key=abc&page=2"
12 changes: 10 additions & 2 deletions tests/httpx2/models/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,16 @@ def test_request_params():
request = httpx2.Request("GET", "http://example.com", params={})
assert str(request.url) == "http://example.com"

# params are appended to, not replacing, any existing query in the URL
request = httpx2.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 = httpx2.Request("GET", "http://example.com?a=1", params={})
assert str(request.url) == "http://example.com"
assert str(request.url) == "http://example.com?a=1"


def test_request_params_no_double_encoding():
# The existing query string must not be reparsed through QueryParams;
# doing so risks double-encoding or reordering of existing parameters.
request = httpx2.Request("GET", "http://example.com?q=hello%20world", params={"page": "2"})
assert str(request.url) == "http://example.com?q=hello%20world&page=2"