Skip to content

fix(presets): raise PresetValidationError, not raw ValueError, on malformed catalog URL#3576

Open
chuenchen309 wants to merge 1 commit into
github:mainfrom
chuenchen309:fix/preset-catalog-url-malformed
Open

fix(presets): raise PresetValidationError, not raw ValueError, on malformed catalog URL#3576
chuenchen309 wants to merge 1 commit into
github:mainfrom
chuenchen309:fix/preset-catalog-url-malformed

Conversation

@chuenchen309

@chuenchen309 chuenchen309 commented Jul 17, 2026

Copy link
Copy Markdown

Description

PresetCatalog._validate_catalog_url leaks a raw ValueError on a malformed catalog URL, violating its documented PresetValidationError contract and crashing the CLI with a traceback.

urlparse("https://[::1").hostname raises ValueError: Invalid IPv6 URL (unterminated bracket). The preset validator accesses .hostname without guarding it, so the exception escapes. Callers only catch PresetValidationError:

  • specify preset catalog add <URL> validates the user's url argument inside except PresetValidationError.
  • specify preset catalog list validates URLs from SPECKIT_PRESET_CATALOG_URL and .specify/preset-catalogs.yml, again catching only PresetValidationError.

So a malformed URL from any of these sources crashes with a traceback instead of a clean error message.

This is the last un-updated copy of a pattern the project has already hardened elsewhere: the shared CatalogStackBase (#3435), workflows (#3484), bundler (#3433), and IntegrationCatalog all wrap this in try/except ValueError and re-raise their domain error. This PR mirrors the shared CatalogStackBase implementation exactly — wrap urlparse + .hostname, re-raise as PresetValidationError("Catalog URL is malformed: ..."), and read the local hostname in the host check.

Before:

$ python -c "from specify_cli.presets import PresetCatalog; from pathlib import Path; PresetCatalog(Path('.'))._validate_catalog_url('https://[::1')"
ValueError: Invalid IPv6 URL      # leaks past callers' except PresetValidationError

After: raises PresetValidationError: Catalog URL is malformed: https://[::1.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (not applicable — pure validation logic)

Added test_validate_catalog_url_malformed_rejected to tests/test_presets.py, mirroring IntegrationCatalog's existing test_malformed_url_rejected_cleanly. It is red on main (raw ValueError) and green with the fix. tests/test_presets.py passes (365), preset-scoped suite passes (376), and uvx ruff check src/ is clean. The valid / host-less / non-HTTPS control cases are unchanged.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (describe below)

Correcting this disclosure — the original wording credited the human operator with work an AI actually did, which understates the extent and would lead you to apply less scrutiny than you should. The accurate version:

This contribution is fully AI-authored and autonomous (Claude Code, acting on this account). An AI found the leak, reproduced it against main, wrote the failing regression test first, mirrored the shared CatalogStackBase fix, ran the preset test suites and ruff check, reviewed the diff, and wrote this description. The human operator of this account did not hand-review the diff or personally run the tests.

What that means for your review, stated plainly against your "what we're looking for" list:

…formed 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` (github#3435), `workflows` (github#3484), `bundler` (github#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>
@chuenchen309
chuenchen309 requested a review from mnriem as a code owner July 17, 2026 04:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant