Skip to content

Commit ae89bfc

Browse files
fix: handle IPv6 bracket notation in address parsing
Added _parse_address() helper that strips brackets from IPv6 addresses like [::1]:9001. Previously rsplit(":", 1) left brackets in the host, which could cause issues with asyncio.open_connection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6f77529 commit ae89bfc

3 files changed

Lines changed: 37 additions & 5 deletions

File tree

src/dqliteclient/cluster.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import asyncio
44

5-
from dqliteclient.connection import DqliteConnection
5+
from dqliteclient.connection import DqliteConnection, _parse_address
66
from dqliteclient.exceptions import ClusterError, DqliteConnectionError, OperationalError
77
from dqliteclient.node_store import MemoryNodeStore, NodeInfo, NodeStore
88
from dqliteclient.protocol import DqliteProtocol
@@ -65,8 +65,7 @@ async def find_leader(self) -> str:
6565

6666
async def _query_leader(self, address: str) -> str | None:
6767
"""Query a node for the current leader."""
68-
host, port_str = address.rsplit(":", 1)
69-
port = int(port_str)
68+
host, port = _parse_address(address)
7069

7170
try:
7271
reader, writer = await asyncio.wait_for(

src/dqliteclient/connection.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@
99
from dqliteclient.protocol import DqliteProtocol
1010

1111

12+
def _parse_address(address: str) -> tuple[str, int]:
13+
"""Parse a host:port address string, handling IPv6 brackets."""
14+
if address.startswith("["):
15+
# Bracketed IPv6: [host]:port
16+
bracket_end = address.index("]")
17+
host = address[1:bracket_end]
18+
port_str = address[bracket_end + 2:] # Skip ']:
19+
else:
20+
host, port_str = address.rsplit(":", 1)
21+
return host, int(port_str)
22+
23+
1224
class DqliteConnection:
1325
"""High-level async connection to a dqlite database."""
1426

@@ -48,8 +60,7 @@ async def connect(self) -> None:
4860
if self._protocol is not None:
4961
return
5062

51-
host, port_str = self._address.rsplit(":", 1)
52-
port = int(port_str)
63+
host, port = _parse_address(self._address)
5364

5465
try:
5566
reader, writer = await asyncio.wait_for(

tests/test_connection.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,28 @@
88
from dqliteclient.exceptions import DqliteConnectionError
99

1010

11+
class TestParseAddress:
12+
def test_ipv4(self) -> None:
13+
from dqliteclient.connection import _parse_address
14+
15+
assert _parse_address("localhost:9001") == ("localhost", 9001)
16+
17+
def test_ipv4_ip(self) -> None:
18+
from dqliteclient.connection import _parse_address
19+
20+
assert _parse_address("192.168.1.1:9001") == ("192.168.1.1", 9001)
21+
22+
def test_ipv6_bracketed(self) -> None:
23+
from dqliteclient.connection import _parse_address
24+
25+
assert _parse_address("[::1]:9001") == ("::1", 9001)
26+
27+
def test_ipv6_full_bracketed(self) -> None:
28+
from dqliteclient.connection import _parse_address
29+
30+
assert _parse_address("[2001:db8::1]:9001") == ("2001:db8::1", 9001)
31+
32+
1133
class TestDqliteConnection:
1234
def test_init(self) -> None:
1335
conn = DqliteConnection("localhost:9001", database="test", timeout=5.0)

0 commit comments

Comments
 (0)