Skip to content

Commit b2adc52

Browse files
committed
Fix query parameter corruption in base_url with trailing slash enforcement
When a base_url contained query parameters, _enforce_trailing_slash() was appending a slash to the entire raw_path (including the query string), which corrupted the query parameter values. For example: - Before: base_url="https://api.com?data=1" became "https://api.com?data=1/" (query param corrupted to "data=1/" instead of "data=1") - After: base_url="https://api.com?data=1" becomes "https://api.com/?data=1" (query param correctly preserved as "data=1") The fix splits the raw_path into path and query components, adds the trailing slash only to the path portion, then recombines them. Fixes #3614
1 parent b5addb6 commit b2adc52

File tree

2 files changed

+46
-3
lines changed

2 files changed

+46
-3
lines changed

httpx/_client.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,19 @@ def trust_env(self) -> bool:
232232
return self._trust_env
233233

234234
def _enforce_trailing_slash(self, url: URL) -> URL:
235-
if url.raw_path.endswith(b"/"):
236-
return url
237-
return url.copy_with(raw_path=url.raw_path + b"/")
235+
# Split raw_path into path and query to avoid corrupting query parameters
236+
raw_path = url.raw_path
237+
if b"?" in raw_path:
238+
# URL has query parameters - only check/add slash to path portion
239+
path, query = raw_path.split(b"?", 1)
240+
if path.endswith(b"/"):
241+
return url
242+
return url.copy_with(raw_path=path + b"/?" + query)
243+
else:
244+
# No query parameters - check/add slash to entire raw_path
245+
if raw_path.endswith(b"/"):
246+
return url
247+
return url.copy_with(raw_path=raw_path + b"/")
238248

239249
def _get_proxy_map(
240250
self, proxy: ProxyTypes | None, allow_env_proxies: bool

tests/client/test_client.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,36 @@ def cp1252_but_no_content_type(request):
460460
assert response.reason_phrase == "OK"
461461
assert response.encoding == "ISO-8859-1"
462462
assert response.text == text
463+
464+
465+
def test_base_url_with_query_params():
466+
"""
467+
Test that base_url with query parameters doesn't corrupt the query values.
468+
469+
Regression test for issue #3614.
470+
"""
471+
client = httpx.Client(base_url="https://example.com/api?data=1")
472+
473+
# Query parameter should not be corrupted with trailing slash
474+
assert client.base_url.query == b"data=1"
475+
assert str(client.base_url) == "https://example.com/api/?data=1"
476+
477+
478+
def test_base_url_with_trailing_slash_and_query():
479+
"""
480+
Test that base_url with existing trailing slash and query params works correctly.
481+
"""
482+
client = httpx.Client(base_url="https://example.com/api/?key=value")
483+
484+
assert client.base_url.query == b"key=value"
485+
assert str(client.base_url) == "https://example.com/api/?key=value"
486+
487+
488+
def test_base_url_with_multiple_query_params():
489+
"""
490+
Test that base_url with multiple query parameters works correctly.
491+
"""
492+
client = httpx.Client(base_url="https://example.com/api?a=1&b=2&c=3")
493+
494+
assert client.base_url.query == b"a=1&b=2&c=3"
495+
assert str(client.base_url) == "https://example.com/api/?a=1&b=2&c=3"

0 commit comments

Comments
 (0)