diff --git a/src/typesense/configuration.py b/src/typesense/configuration.py index 34df662..0d2b0ab 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 = str(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/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: 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"