From 18927fa8e80fcf650d2c47cfe370a7d92c18bd83 Mon Sep 17 00:00:00 2001 From: Friday Date: Wed, 18 Feb 2026 04:34:10 +0000 Subject: [PATCH] Fix base_url query parameters being corrupted during URL merging When base_url contains query parameters (e.g. "https://example.com/get?data=1"), _enforce_trailing_slash() appended "/" to raw_path which includes the query string, corrupting "?data=1" into "?data=1/". Similarly, _merge_url() concatenated paths without separating the query string from the path component. Fix both methods to split raw_path at "?" before manipulating the path, then rejoin with the query string intact. Fixes #3614 Co-Authored-By: Claude Opus 4.6 --- httpx/_client.py | 24 +++++++++++++++++++++--- tests/client/test_client.py | 11 +++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/httpx/_client.py b/httpx/_client.py index 13cd933673..f80e83505d 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -232,9 +232,15 @@ def trust_env(self) -> bool: return self._trust_env def _enforce_trailing_slash(self, url: URL) -> URL: - if url.raw_path.endswith(b"/"): + raw_path = url.raw_path + if b"?" in raw_path: + path_part, query_part = raw_path.split(b"?", 1) + if path_part.endswith(b"/"): + return url + return url.copy_with(raw_path=path_part + b"/?" + query_part) + if raw_path.endswith(b"/"): return url - return url.copy_with(raw_path=url.raw_path + b"/") + return url.copy_with(raw_path=raw_path + b"/") def _get_proxy_map( self, proxy: ProxyTypes | None, allow_env_proxies: bool @@ -406,7 +412,19 @@ def _merge_url(self, url: URL | str) -> URL: # URL('https://www.example.com/subpath/') # >>> client.build_request("GET", "/path").url # URL('https://www.example.com/subpath/path') - merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") + base_raw_path = self.base_url.raw_path + if b"?" in base_raw_path: + base_path, base_query = base_raw_path.split(b"?", 1) + merge_raw_path = ( + base_path + + merge_url.raw_path.lstrip(b"/") + + b"?" + + base_query + ) + else: + merge_raw_path = ( + base_raw_path + merge_url.raw_path.lstrip(b"/") + ) return self.base_url.copy_with(raw_path=merge_raw_path) return merge_url diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 657839018a..977e58996b 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -229,6 +229,17 @@ def test_merge_relative_url_with_encoded_slashes(): assert request.url == "https://www.example.com/base%2Fpath/testing" +def test_merge_url_with_base_url_query_params(): + # https://github.com/encode/httpx/issues/3614 + client = httpx.Client(base_url="https://www.example.com/get?data=1") + + request = client.build_request("GET", "") + assert str(request.url) == "https://www.example.com/get/?data=1" + + request = client.build_request("GET", "/users") + assert str(request.url) == "https://www.example.com/get/users?data=1" + + def test_context_managed_transport(): class Transport(httpx.BaseTransport): def __init__(self) -> None: