From 43463e8964aa4fb8b55e55e04cfb04d49c2e6a54 Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:13:19 -0700 Subject: [PATCH] fix: degrade gracefully on malformed URLs in _parse urlsplit raises ValueError on some malformed inputs (e.g. a bad IPv6 literal like HTTP://[::1]&). This leaked out of get_base_url and get_hostinfo, which are contracted to return a value for any string. Guard _parse so those inputs degrade to empty parts, matching how a hostless URL already parses. get_host_and_path still raises its own documented incomplete-URL error. --- courlan/urlutils.py | 6 +++++- tests/unit_tests.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/courlan/urlutils.py b/courlan/urlutils.py index 3f5692d..c21759e 100644 --- a/courlan/urlutils.py +++ b/courlan/urlutils.py @@ -61,7 +61,11 @@ def extract_domain( def _parse(url: str | SplitResult) -> SplitResult: "Parse a string or use urllib.parse object directly." if isinstance(url, str): - parsed_url = urlsplit(unescape(url)) + try: + parsed_url = urlsplit(unescape(url)) + except ValueError: + # malformed URL (e.g. bad IPv6 literal): degrade to empty parts like a hostless URL. + parsed_url = SplitResult("", "", "", "", "") elif isinstance(url, SplitResult): parsed_url = url else: diff --git a/tests/unit_tests.py b/tests/unit_tests.py index 4ca5120..41293ce 100644 --- a/tests/unit_tests.py +++ b/tests/unit_tests.py @@ -59,6 +59,22 @@ def test_baseurls(): assert get_base_url("example.org") == "" +def test_malformed_url_degrades(): + # A malformed URL (bad IPv6 literal) made urlsplit raise a raw ValueError that + # leaked out of get_base_url/get_hostinfo; it now degrades like a hostless URL. + assert get_base_url("HTTP://[::1]&") == "" + assert get_hostinfo("HTTP://[::1]&") == (None, "") + # get_host_and_path still raises its own documented incomplete-URL error. + with pytest.raises(ValueError, match="incomplete URL"): + get_host_and_path("HTTP://[::1]&") + # valid and hostless URLs are unaffected. + assert get_base_url("https://example.org/") == "https://example.org" + assert get_hostinfo("https://example.org/path") == ( + "example.org", + "https://example.org", + ) + + def test_fix_relative(): assert ( fix_relative_urls("https://example.org", "page.html")