Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.19.0
3.19.1
4 changes: 2 additions & 2 deletions chargebee/http_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion chargebee/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = "3.19.0"
VERSION = "3.19.1"
45 changes: 44 additions & 1 deletion tests/test_http_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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"""
Expand Down
Loading