Skip to content

Commit 0446d8d

Browse files
chuenchen309claude
andcommitted
fix(presets): raise PresetValidationError, not raw ValueError, on malformed catalog URL
`PresetCatalog._validate_catalog_url` called `urlparse(url).hostname` without guarding it. For a malformed authority such as an unterminated IPv6 bracket (`https://[::1`), `urlparse(...).hostname` raises `ValueError: Invalid IPv6 URL`, which escapes the method. Its docstring promises `PresetValidationError`, and its callers (`preset catalog add`, `preset catalog list` reading the `SPECKIT_PRESET_CATALOG_URL` env var / `.specify/preset-catalogs.yml`) only catch `PresetValidationError` -- so a malformed URL crashes the CLI with a traceback instead of a clean error message. The shared `CatalogStackBase` (#3435), `workflows` (#3484), `bundler` (#3433) and `IntegrationCatalog` copies already wrap this in `try/except ValueError`; the preset validator was the remaining un-updated twin. Mirror the shared implementation: wrap `urlparse` + `.hostname`, re-raise as `PresetValidationError("Catalog URL is malformed: ...")`, and read the local `hostname` in the host check. Add a regression test mirroring `IntegrationCatalog`'s `test_malformed_url_rejected_cleanly`; it is red before the fix (raw `ValueError`) and green after. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7bdf6c5 commit 0446d8d

2 files changed

Lines changed: 19 additions & 3 deletions

File tree

src/specify_cli/presets/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2074,8 +2074,12 @@ def _validate_catalog_url(self, url: str) -> None:
20742074
"""
20752075
from urllib.parse import urlparse
20762076

2077-
parsed = urlparse(url)
2078-
is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1")
2077+
try:
2078+
parsed = urlparse(url)
2079+
hostname = parsed.hostname
2080+
except ValueError:
2081+
raise PresetValidationError(f"Catalog URL is malformed: {url}") from None
2082+
is_localhost = hostname in ("localhost", "127.0.0.1", "::1")
20792083
if parsed.scheme != "https" and not (
20802084
parsed.scheme == "http" and is_localhost
20812085
):
@@ -2086,7 +2090,7 @@ def _validate_catalog_url(self, url: str) -> None:
20862090
# Check hostname, not netloc: netloc is truthy for host-less URLs like
20872091
# "https://:8080" or "https://user@", so the host guarantee this error
20882092
# promises would not actually hold. hostname is None in those cases (#3209).
2089-
if not parsed.hostname:
2093+
if not hostname:
20902094
raise PresetValidationError(
20912095
"Catalog URL must be a valid URL with a host."
20922096
)

tests/test_presets.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1634,6 +1634,18 @@ def test_validate_catalog_url_hostless_rejected(self, project_dir, url):
16341634
with pytest.raises(PresetValidationError, match="valid URL with a host"):
16351635
catalog._validate_catalog_url(url)
16361636

1637+
def test_validate_catalog_url_malformed_rejected(self, project_dir):
1638+
"""A malformed URL raises PresetValidationError, not a raw ValueError.
1639+
1640+
``urlparse('https://[::1').hostname`` raises ``ValueError: Invalid IPv6
1641+
URL`` (unterminated bracket). Without wrapping, that leaks past callers'
1642+
``except PresetValidationError`` guards and crashes the CLI. Mirrors the
1643+
shared ``CatalogStackBase`` (#3435) and ``IntegrationCatalog`` behaviour.
1644+
"""
1645+
catalog = PresetCatalog(project_dir)
1646+
with pytest.raises(PresetValidationError, match="malformed"):
1647+
catalog._validate_catalog_url("https://[::1")
1648+
16371649
def test_env_var_catalog_url(self, project_dir, monkeypatch):
16381650
"""Test catalog URL from environment variable."""
16391651
monkeypatch.setenv("SPECKIT_PRESET_CATALOG_URL", "https://custom.example.com/catalog.json")

0 commit comments

Comments
 (0)