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")