github/spec-kit · error · BundlerError

Unsupported catalog url scheme '{parsed.scheme}://' in '{url

Error message

Unsupported catalog url scheme '{parsed.scheme}://' in '{url}'. Use http(s)://, file://, builtin://, or a local path.

What it means

Raised when the URL contains `://` but its scheme is not in `_REMOTE_SCHEMES` (http/https, file, builtin). Unsupported schemes like `ssh://` or `ftp://` are rejected up front so they are never silently canonicalized into local filesystem paths. Local paths containing `:` without `://` (Windows drive letters like `C:\catalog.json`) are explicitly still allowed.

Source

Thrown at src/specify_cli/bundler/commands_impl/catalog_config.py:165

        parsed = urlparse(url)
        # Read .hostname inside the try: a bracketed-but-invalid IPv6 authority
        # (e.g. "https://[not-an-ip]/c.json") parses cleanly under urlparse() on
        # Python < 3.14 but raises ValueError lazily on the first .hostname access
        # (the raise moved eager into urlparse() only in 3.14). Reading it here
        # keeps that ValueError inside the guard instead of leaking a raw
        # traceback past the CLI's `except BundlerError`. Reuse the value below.
        hostname = parsed.hostname
        # Accessing ``port`` performs urllib's syntax/range validation.
        _ = parsed.port
    except ValueError as exc:
        raise BundlerError(f"Invalid catalog url: '{url}'.") from exc
    if not (parsed.scheme or parsed.path):
        raise BundlerError(f"Invalid catalog url: '{url}'.")
    # Reject unsupported URL schemes (e.g. ssh://, ftp://) up front so they are
    # never silently canonicalized as local filesystem paths. Local paths that
    # merely contain a ':' but no '://' (e.g. Windows drives) are still allowed.
    if "://" in url and parsed.scheme.lower() not in _REMOTE_SCHEMES:
        raise BundlerError(
            f"Unsupported catalog url scheme '{parsed.scheme}://' in '{url}'. "
            "Use http(s)://, file://, builtin://, or a local path."
        )
    if parsed.scheme.lower() in {"http", "https"}:
        # Mirror specify_cli.catalogs._validate_catalog_url (#3209/#3210):
        # HTTPS only (HTTP just for localhost), and check hostname, not
        # netloc — netloc is truthy for host-less URLs like "https://:8080"
        # or "https://user@". Validating here keeps junk out of
        # bundle-catalogs.yml instead of failing later at fetch time.
        is_localhost = hostname in ("localhost", "127.0.0.1", "::1")
        if parsed.scheme.lower() != "https" and not is_localhost:
            raise BundlerError(
                f"Catalog url must use HTTPS (got {parsed.scheme}://). "
                "HTTP is only allowed for localhost."
            )
        if not hostname:
            raise BundlerError(f"Catalog url must be a valid URL with a host: {url}")

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Serve the catalog over http(s)://, file://, builtin://, or point at a local file path
  2. For git-hosted catalogs, use the raw/https URL of the JSON file, not the clone URL
  3. For local files use a plain path (POSIX) — no scheme needed

Example fix

# before
ssh://git@example.com/spec-kit/catalogs.json
git://example.com/catalogs.json

# after
https://example.com/catalogs.json
file:///opt/spec-kit/catalogs.json
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse
REMOTE_SCHEMES = {"http", "https", "file", "builtin"}

u = url.strip()
p = urlparse(u)
if "://" in u and p.scheme.lower() not in REMOTE_SCHEMES:
    raise SystemExit(f"scheme {p.scheme!r} unsupported; use http(s)://, file://, builtin://, or a path")

Type guard

def is_supported_catalog_url(url: str) -> bool:
    u = url.strip()
    p = urlparse(u)
    return "://" not in u or p.scheme.lower() in {"http", "https", "file", "builtin"}

Try / catch

try:
    add_source(project_root, url, policy=policy, priority=50)
except BundlerError as exc:
    if "Unsupported catalog url scheme" in str(exc):
        # swap git/ssh clone URLs for the raw https/file URL of the catalog JSON
        raise
    raise

Prevention

When it happens

Trigger: `ssh://git@host/catalog.json`, `ftp://host/c.json`, `git://...`, or any `x://...` URL whose scheme is not in `_REMOTE_SCHEMES`.

Common situations: Pasting a git clone URL instead of the raw catalog JSON URL; internal tooling URLs using custom schemes; copying from an Ansible/CI variable that expects scp-style syntax.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/abfcc413c069e6c7. Report an issue: GitHub.