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
- Include the hostname: `https://example.com/catalog.json`
- Check how the URL string was assembled — the host component is missing
- 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
- Check urlparse(url).hostname before persisting URLs built from variables
- Note netloc lies: https://:8080 is truthy netloc but empty hostname — validate hostname
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
- Invalid catalog url: '{url}'.
- Unsupported catalog url scheme '{parsed.scheme}://' in '{url
- Catalog entry '{resolved.entry.id}' has a malformed download
- Malformed catalog config at {path}: expected a mapping at th
- Malformed catalog config at {path}: 'catalogs' must be a lis
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/33f9590307d844ef.
Report an issue: GitHub.