diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index 98bba08fd4..97e15aa648 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -2074,8 +2074,12 @@ def _validate_catalog_url(self, url: str) -> None: """ from urllib.parse import urlparse - parsed = urlparse(url) - is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1") + try: + parsed = urlparse(url) + hostname = parsed.hostname + except ValueError: + raise PresetValidationError(f"Catalog URL is malformed: {url}") from None + is_localhost = hostname in ("localhost", "127.0.0.1", "::1") if parsed.scheme != "https" and not ( parsed.scheme == "http" and is_localhost ): @@ -2086,7 +2090,7 @@ def _validate_catalog_url(self, url: str) -> None: # Check hostname, not netloc: netloc is truthy for host-less URLs like # "https://:8080" or "https://user@", so the host guarantee this error # promises would not actually hold. hostname is None in those cases (#3209). - if not parsed.hostname: + if not hostname: raise PresetValidationError( "Catalog URL must be a valid URL with a host." ) diff --git a/tests/test_presets.py b/tests/test_presets.py index 797835a88c..aec335c916 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -1634,6 +1634,18 @@ def test_validate_catalog_url_hostless_rejected(self, project_dir, url): with pytest.raises(PresetValidationError, match="valid URL with a host"): catalog._validate_catalog_url(url) + def test_validate_catalog_url_malformed_rejected(self, project_dir): + """A malformed URL raises PresetValidationError, not a raw ValueError. + + ``urlparse('https://[::1').hostname`` raises ``ValueError: Invalid IPv6 + URL`` (unterminated bracket). Without wrapping, that leaks past callers' + ``except PresetValidationError`` guards and crashes the CLI. Mirrors the + shared ``CatalogStackBase`` (#3435) and ``IntegrationCatalog`` behaviour. + """ + catalog = PresetCatalog(project_dir) + with pytest.raises(PresetValidationError, match="malformed"): + catalog._validate_catalog_url("https://[::1") + def test_env_var_catalog_url(self, project_dir, monkeypatch): """Test catalog URL from environment variable.""" monkeypatch.setenv("SPECKIT_PRESET_CATALOG_URL", "https://custom.example.com/catalog.json")