From 83ea751123febd0a61af1cd62232355d6d987ab4 Mon Sep 17 00:00:00 2001 From: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:14:55 +0000 Subject: [PATCH 1/3] fix(config): enclose IPv6 node hosts in brackets when building the URL --- src/typesense/configuration.py | 6 +++++- tests/node_test.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/typesense/configuration.py b/src/typesense/configuration.py index 34df662..89c3cf1 100644 --- a/src/typesense/configuration.py +++ b/src/typesense/configuration.py @@ -203,7 +203,11 @@ def url(self) -> str: Returns: str: The URL of the node """ - return f"{self.protocol}://{self.host}:{self.port}{self.path}" + host = self.host + if ":" in host and not host.startswith("["): + # IPv6 literals must be enclosed in brackets in a URL. + host = f"[{host}]" + return f"{self.protocol}://{host}:{self.port}{self.path}" class Configuration: diff --git a/tests/node_test.py b/tests/node_test.py index 23ab20d..74727ab 100644 --- a/tests/node_test.py +++ b/tests/node_test.py @@ -63,3 +63,21 @@ def test_node_url() -> None: """Test the URL method of the Node class.""" node = Node(host="localhost", port=8108, path="/path", protocol="http") assert node.url() == "http://localhost:8108/path" + + +def test_node_url_ipv6_host() -> None: + """Test that the URL method encloses an IPv6 host in brackets.""" + node = Node(host="::1", port=8108, path="/path", protocol="http") + assert node.url() == "http://[::1]:8108/path" + + +def test_node_url_bracketed_ipv6_host() -> None: + """Test that the URL method keeps an already bracketed IPv6 host as is.""" + node = Node(host="[::1]", port=8108, path="/path", protocol="http") + assert node.url() == "http://[::1]:8108/path" + + +def test_node_from_url_ipv6() -> None: + """Test that a node built from an IPv6 URL generates the same URL.""" + node = Node.from_url("http://[::1]:8108/path") + assert node.url() == "http://[::1]:8108/path" From 7568ae78147d45e949fa38db5adade03da162571 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Thu, 22 Oct 2026 17:19:26 +0300 Subject: [PATCH 2/3] fix(config): keep non-string node hosts working when building the URL --- src/typesense/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typesense/configuration.py b/src/typesense/configuration.py index 89c3cf1..0d2b0ab 100644 --- a/src/typesense/configuration.py +++ b/src/typesense/configuration.py @@ -203,7 +203,7 @@ def url(self) -> str: Returns: str: The URL of the node """ - host = self.host + host = str(self.host) if ":" in host and not host.startswith("["): # IPv6 literals must be enclosed in brackets in a URL. host = f"[{host}]" From c86e61287865df2f20f6df83ad7be7c4d14ed90c Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Thu, 22 Oct 2026 17:19:26 +0300 Subject: [PATCH 3/3] test(api-call): cover requests to ipv6 nodes --- tests/api_call_test.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/api_call_test.py b/tests/api_call_test.py index d4684d2..2a6a12c 100644 --- a/tests/api_call_test.py +++ b/tests/api_call_test.py @@ -23,7 +23,7 @@ from typesense import exceptions from typesense.sync.api_call import ApiCall, RequestHandler from typesense.async_.api_call import AsyncApiCall -from typesense.configuration import Configuration, Node +from typesense.configuration import Configuration, Node, NodeConfigDict from typesense.logger import logger @@ -390,6 +390,23 @@ def test_delete_as_json( assert response == {"key": "value"} +@pytest.mark.parametrize( + "node", + ["http://[::1]:8108", {"host": "::1", "port": 8108, "protocol": "http"}], +) +def test_request_to_ipv6_node(node: typing.Union[str, NodeConfigDict]) -> None: + """Test that a request to an IPv6 node is sent to the bracketed host.""" + api_call = ApiCall(Configuration({"api_key": "xyz", "nodes": [node]})) + with respx.mock: + route = respx.get("http://[::1]:8108/test").mock( + return_value=httpx.Response(200, json={"key": "value"}), + ) + + response = api_call.get("/test", entity_type=typing.Dict[str, str]) + assert response == {"key": "value"} + assert route.called + + def test_raise_custom_exception_with_header( fake_api_call: ApiCall, ) -> None: