{"record":{"id":"33f9590307d844ef","repo":"github/spec-kit","slug":"catalog-url-must-be-a-valid-url-with-a-host-url","errorCode":null,"errorMessage":"Catalog url must be a valid URL with a host: {url}","messagePattern":"Catalog url must be a valid URL with a host: (.+?)","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/bundler/commands_impl/catalog_config.py","lineNumber":182,"sourceCode":"    if \"://\" in url and parsed.scheme.lower() not in _REMOTE_SCHEMES:\n        raise BundlerError(\n            f\"Unsupported catalog url scheme '{parsed.scheme}://' in '{url}'. \"\n            \"Use http(s)://, file://, builtin://, or a local path.\"\n        )\n    if parsed.scheme.lower() in {\"http\", \"https\"}:\n        # Mirror specify_cli.catalogs._validate_catalog_url (#3209/#3210):\n        # HTTPS only (HTTP just for localhost), and check hostname, not\n        # netloc — netloc is truthy for host-less URLs like \"https://:8080\"\n        # or \"https://user@\". Validating here keeps junk out of\n        # bundle-catalogs.yml instead of failing later at fetch time.\n        is_localhost = hostname in (\"localhost\", \"127.0.0.1\", \"::1\")\n        if parsed.scheme.lower() != \"https\" and not is_localhost:\n            raise BundlerError(\n                f\"Catalog url must use HTTPS (got {parsed.scheme}://). \"\n                \"HTTP is only allowed for localhost.\"\n            )\n        if not hostname:\n            raise BundlerError(f\"Catalog url must be a valid URL with a host: {url}\")\n\n    url = _canonicalize_url(url)\n    install_policy = InstallPolicy.parse(policy)\n    resolved_id = (source_id or _derive_id(url)).strip()\n\n    catalogs = _read(project_root)\n    for existing in catalogs:\n        if existing.get(\"id\") == resolved_id or existing.get(\"url\") == url:\n            raise BundlerError(\n                f\"Catalog source '{resolved_id}' (or url) already exists in this project.\"\n            )\n\n    entry = {\n        \"id\": resolved_id,\n        \"url\": url,\n        \"priority\": int(priority),\n        \"install_policy\": install_policy.value,\n    }","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/commands_impl/catalog_config.py#L164-L200","documentation":"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.","triggerScenarios":"`https://:8080/catalog.json`, `https://user@/c.json`, `https:///path/c.json` — any http(s) URL where urlparse yields an empty hostname.","commonSituations":"URL assembled from variables where the host variable was empty; stray `@` from user:pass templating with no host; extra slash after the scheme.","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"],"exampleFix":"# before\nhttps://${HOST:-}/catalog.json   # renders https:///catalog.json\n\n# after\nCATALOG_HOST=\"${CATALOG_HOST:?required}\"\nhttps://${CATALOG_HOST}/catalog.json","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\np = urlparse(url)\nif p.scheme.lower() in {\"http\", \"https\"} and not p.hostname:\n    raise SystemExit(f\"{url!r} has no host — check empty host variables and stray '@'\")","typeGuard":"def has_http_hostname(url: str) -> bool:\n    p = urlparse(url)\n    return p.scheme.lower() not in {\"http\", \"https\"} or bool(p.hostname)","tryCatchPattern":"try:\n    add_source(project_root, url, policy=policy, priority=50)\nexcept BundlerError as exc:\n    if \"must be a valid URL with a host\" in str(exc):\n        # the URL assembled to https://:8080/… or https://user@/…; fix the host component\n        raise\n    raise","preventionTips":["Check urlparse(url).hostname before persisting URLs built from variables","Note netloc lies: https://:8080 is truthy netloc but empty hostname — validate hostname"],"tags":["bundler","catalog","url","validation","hostname"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}