github/spec-kit · error · BundlerError

Catalog url must be a valid URL with a host: {url}

Error message

Catalog url must be a valid URL with a host: {url}

What it means

Raised for http/https catalog URLs whose parsed `hostname` is empty. The check deliberately uses `hostname`, not `netloc`, because `netloc` is truthy for host-less URLs like `https://:8080` or `https://user@` — a credential/port without a host is junk that would only fail later at fetch time, so it is kept out of `bundle-catalogs.yml` at add time.

Source

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

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

    url = _canonicalize_url(url)
    install_policy = InstallPolicy.parse(policy)
    resolved_id = (source_id or _derive_id(url)).strip()

    catalogs = _read(project_root)
    for existing in catalogs:
        if existing.get("id") == resolved_id or existing.get("url") == url:
            raise BundlerError(
                f"Catalog source '{resolved_id}' (or url) already exists in this project."
            )

    entry = {
        "id": resolved_id,
        "url": url,
        "priority": int(priority),
        "install_policy": install_policy.value,
    }

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Include the hostname: `https://example.com/catalog.json`
  2. Check how the URL string was assembled — the host component is missing
  3. Add a caller-side assertion that urlparse(url).hostname is non-empty

Example fix

# before
https://${HOST:-}/catalog.json   # renders https:///catalog.json

# after
CATALOG_HOST="${CATALOG_HOST:?required}"
https://${CATALOG_HOST}/catalog.json
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse

p = urlparse(url)
if p.scheme.lower() in {"http", "https"} and not p.hostname:
    raise SystemExit(f"{url!r} has no host — check empty host variables and stray '@'")

Type guard

def has_http_hostname(url: str) -> bool:
    p = urlparse(url)
    return p.scheme.lower() not in {"http", "https"} or bool(p.hostname)

Try / catch

try:
    add_source(project_root, url, policy=policy, priority=50)
except BundlerError as exc:
    if "must be a valid URL with a host" in str(exc):
        # the URL assembled to https://:8080/… or https://user@/…; fix the host component
        raise
    raise

Prevention

When it happens

Trigger: `https://:8080/catalog.json`, `https://user@/c.json`, `https:///path/c.json` — any http(s) URL where urlparse yields an empty hostname.

Common situations: URL assembled from variables where the host variable was empty; stray `@` from user:pass templating with no host; extra slash after the scheme.

Related errors


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