{"record":{"id":"537b9b06965ea9c1","repo":"github/spec-kit","slug":"catalog-url-must-use-https-got-parsed-scheme","errorCode":null,"errorMessage":"Catalog url must use HTTPS (got {parsed.scheme}://). HTTP is only allowed for localhost.","messagePattern":"Catalog url must use HTTPS \\(got (.+?)://\\)\\. HTTP is only allowed for localhost\\.","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/bundler/commands_impl/catalog_config.py","lineNumber":177,"sourceCode":"    if not (parsed.scheme or parsed.path):\n        raise BundlerError(f\"Invalid catalog url: '{url}'.\")\n    # Reject unsupported URL schemes (e.g. ssh://, ftp://) up front so they are\n    # never silently canonicalized as local filesystem paths. Local paths that\n    # merely contain a ':' but no '://' (e.g. Windows drives) are still allowed.\n    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 = {","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/commands_impl/catalog_config.py#L159-L195","documentation":"Raised for http/https catalog URLs when the scheme is plain `http` and the hostname is not a loopback address (`localhost`, `127.0.0.1`, `::1`). This mirrors the fetch-time rule from `specify_cli.catalogs._validate_catalog_url` (#3209/#3210) so plaintext HTTP catalogs are caught at config time, not at fetch time.","triggerScenarios":"`http://catalog.internal.example.com/c.json` — any non-localhost http URL. Note `https://…` never triggers this; only scheme!=https with a non-loopback host does.","commonSituations":"Internal company mirrors served over plain HTTP; local dev against a non-loopback IP (e.g. docker host `http://172.17.0.1`); stale bookmark of a site that later added TLS.","solutions":["Switch to the https:// version of the URL","For local development, use `http://localhost...`, `http://127.0.0.1...`, or `http://[::1]...`","Put an TLS-terminating proxy in front of an internal HTTP mirror"],"exampleFix":"# before\nhttp://catalog.internal.example.com/catalog.json\n\n# after\nhttps://catalog.internal.example.com/catalog.json\n# or for local dev:\nhttp://localhost:8080/catalog.json","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\np = urlparse(url)\nif p.scheme.lower() in {\"http\", \"https\"}:\n    host = p.hostname\n    if p.scheme.lower() != \"https\" and host not in (\"localhost\", \"127.0.0.1\", \"::1\"):\n        raise SystemExit(\"non-localhost HTTP catalogs are rejected — use https or localhost\")","typeGuard":"def is_https_or_localhost(url: str) -> bool:\n    p = urlparse(url)\n    return p.scheme.lower() != \"http\" or p.hostname in (\"localhost\", \"127.0.0.1\", \"::1\")","tryCatchPattern":"try:\n    add_source(project_root, url, policy=policy, priority=50)\nexcept BundlerError as exc:\n    if \"must use HTTPS\" in str(exc):\n        # rewrite http://host -> https://host, or use http://localhost for dev\n        raise\n    raise","preventionTips":["Default to https:// when writing catalog URLs; http only for loopback testing","For dockerized local catalogs, publish the port on localhost rather than hitting the container IP over http"],"tags":["bundler","catalog","https","security","url"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}