From 88e544e82da0a9f228a8ed9c50b75214cd12601c Mon Sep 17 00:00:00 2001 From: Kel Modderman Date: Mon, 17 Aug 2026 14:16:33 +1000 Subject: [PATCH] Bracket bare IPv6 literal hosts in URLs A host like fd00::1 interpolated raw into the http/ws/image URL f-strings produces e.g. ws://fd00::1:9090/jsonrpc, which aiohttp rejects with InvalidUrlClientError before any connection attempt (RFC 3986 requires IPv6 literals bracketed). In Home Assistant this surfaces as a Kodi entry permanently 'off' with 'Unable to connect to Kodi via websocket' after zeroconf updates the entry host to an IPv6 address - the device is healthy and reachable, only the URL is malformed. Verified against a live Kodi over a bare ULA host: unpatched raises InvalidUrlClientError, patched connects and pings. Already-bracketed hosts, IPv4 and hostnames pass through unchanged. Co-Authored-By: Claude Fable 5 --- pykodi/kodi.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/pykodi/kodi.py b/pykodi/kodi.py index 2cbb5eb..6a7cd42 100644 --- a/pykodi/kodi.py +++ b/pykodi/kodi.py @@ -20,6 +20,17 @@ def get_kodi_connection( ) +def _url_host(host): + """Return a host formatted for use in a URL. + + Bare IPv6 literals must be bracketed in URLs (RFC 3986 section 3.2.2), + otherwise their colons are parsed as port separators. + """ + if ":" in host and not host.startswith("["): + return f"[{host}]" + return host + + class KodiConnection: """A connection to Kodi interface.""" @@ -41,7 +52,7 @@ def __init__(self, host, port, username, password, ssl, timeout, session): http_protocol = "https" if ssl else "http" - self._image_url = f"{http_protocol}://{image_auth_string}{host}:{port}/image" + self._image_url = f"{http_protocol}://{image_auth_string}{_url_host(host)}:{port}/image" async def connect(self): """Connect to kodi.""" @@ -86,7 +97,7 @@ def __init__(self, host, port, username, password, ssl, timeout, session): http_protocol = "https" if ssl else "http" - http_url = f"{http_protocol}://{host}:{port}/jsonrpc" + http_url = f"{http_protocol}://{_url_host(host)}:{port}/jsonrpc" self._http_server = jsonrpc_async.Server(http_url, **self._kwargs) @@ -114,7 +125,7 @@ def __init__(self, host, port, ws_port, username, password, ssl, timeout, sessio super().__init__(host, port, username, password, ssl, timeout, session) ws_protocol = "wss" if ssl else "ws" - ws_url = f"{ws_protocol}://{host}:{ws_port}/jsonrpc" + ws_url = f"{ws_protocol}://{_url_host(host)}:{ws_port}/jsonrpc" self._ws_server = jsonrpc_websocket.Server(ws_url, **self._kwargs)