diff --git a/CHANGELOG.md b/CHANGELOG.md index bc25033..0716942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### v3.19.1 (2026-03-11) +* * * +### Bug Fixes: +- Fixed URL construction to avoid appending a trailing `?` when there are no query parameters. + ### v3.19.0 (2026-03-02) * * * ### New Resources: diff --git a/VERSION b/VERSION index 419f300..4f6817b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.19.0 +3.19.1 diff --git a/chargebee/http_request.py b/chargebee/http_request.py index a85d898..37fdd1a 100644 --- a/chargebee/http_request.py +++ b/chargebee/http_request.py @@ -81,8 +81,8 @@ def request( meta = compat.urlparse(url) scheme = "https" if Chargebee.verify_ca_certs or env.protocol == "https" else "http" - full_url = f"{scheme}://{meta.netloc + meta.path + '?' + meta.query}" - + base = f"{scheme}://{meta.netloc}{meta.path}" + full_url = f"{base}?{meta.query}" if meta.query else base timeout = httpx.Timeout( None, connect=env.connect_timeout, diff --git a/chargebee/version.py b/chargebee/version.py index 9f92b45..70e1ba4 100644 --- a/chargebee/version.py +++ b/chargebee/version.py @@ -1 +1 @@ -VERSION = "3.19.0" +VERSION = "3.19.1" diff --git a/tests/test_http_request.py b/tests/test_http_request.py index ea3ec3b..d0b131d 100644 --- a/tests/test_http_request.py +++ b/tests/test_http_request.py @@ -275,7 +275,7 @@ def test_subdomain_url(self, mock_client_class): call_args = mock_client.request.call_args self.assertEqual( - call_args[1]["url"], "https://test_site.ingest.chargebee.com/api/v2/test?" + call_args[1]["url"], "https://test_site.ingest.chargebee.com/api/v2/test" ) @patch("httpx.Client") @@ -331,6 +331,49 @@ class Priority(Enum): self.assertEqual(json_data["items"][1]["status"], "inactive") self.assertEqual(json_data["items"][1]["priority"], 3) + @patch("httpx.Client") + def test_url_no_trailing_question_mark_without_query_params( + self, mock_client_class + ): + """Test that URLs without query params don't get a trailing '?'""" + mock_client, mock_response = make_mock_client( + text=json.dumps({"message": "success"}) + ) + mock_client.request.return_value = mock_response + mock_client_class.return_value.__enter__.return_value = mock_client + + from chargebee.http_request import request + + request("POST", "/test", MockEnvironment(), params={"key": "value"}) + + call_args = mock_client.request.call_args + url = call_args[1]["url"] + self.assertFalse(url.endswith("?"), f"URL should not end with '?', got: {url}") + + @patch("httpx.Client") + def test_url_preserves_query_string_when_present(self, mock_client_class): + """Test that URLs with an existing query string are preserved correctly""" + mock_client, mock_response = make_mock_client( + text=json.dumps({"message": "success"}) + ) + mock_client.request.return_value = mock_response + mock_client_class.return_value.__enter__.return_value = mock_client + + from chargebee.http_request import request + + class MockEnvironmentWithQuery(MockEnvironment): + def api_url(self, url, subDomain=None): + return super().api_url(url, subDomain) + "?existing=param" + + request("GET", "/test", MockEnvironmentWithQuery(), params={}) + + call_args = mock_client.request.call_args + url = call_args[1]["url"] + self.assertIn("?existing=param", url) + self.assertFalse( + url.endswith("?"), f"URL should not end with bare '?', got: {url}" + ) + @patch("httpx.Client") def test_form_request_without_enum_conversion(self, mock_client_class): """Test that form requests (non-JSON) don't use convert_to_serializable"""