{"record":{"id":"abfcc413c069e6c7","repo":"github/spec-kit","slug":"unsupported-catalog-url-scheme-parsed-scheme","errorCode":null,"errorMessage":"Unsupported catalog url scheme '{parsed.scheme}://' in '{url}'. Use http(s)://, file://, builtin://, or a local path.","messagePattern":"Unsupported catalog url scheme '(.+?)://' in '(.+?)'\\. Use http\\(s\\)://, file://, builtin://, or a local path\\.","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/bundler/commands_impl/catalog_config.py","lineNumber":165,"sourceCode":"        parsed = urlparse(url)\n        # Read .hostname inside the try: a bracketed-but-invalid IPv6 authority\n        # (e.g. \"https://[not-an-ip]/c.json\") parses cleanly under urlparse() on\n        # Python < 3.14 but raises ValueError lazily on the first .hostname access\n        # (the raise moved eager into urlparse() only in 3.14). Reading it here\n        # keeps that ValueError inside the guard instead of leaking a raw\n        # traceback past the CLI's `except BundlerError`. Reuse the value below.\n        hostname = parsed.hostname\n        # Accessing ``port`` performs urllib's syntax/range validation.\n        _ = parsed.port\n    except ValueError as exc:\n        raise BundlerError(f\"Invalid catalog url: '{url}'.\") from exc\n    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","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/commands_impl/catalog_config.py#L147-L183","documentation":"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.","triggerScenarios":"`ssh://git@host/catalog.json`, `ftp://host/c.json`, `git://...`, or any `x://...` URL whose scheme is not in `_REMOTE_SCHEMES`.","commonSituations":"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.","solutions":["Serve the catalog over http(s)://, file://, builtin://, or point at a local file path","For git-hosted catalogs, use the raw/https URL of the JSON file, not the clone URL","For local files use a plain path (POSIX) — no scheme needed"],"exampleFix":"# before\nssh://git@example.com/spec-kit/catalogs.json\ngit://example.com/catalogs.json\n\n# after\nhttps://example.com/catalogs.json\nfile:///opt/spec-kit/catalogs.json","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\nREMOTE_SCHEMES = {\"http\", \"https\", \"file\", \"builtin\"}\n\nu = url.strip()\np = urlparse(u)\nif \"://\" in u and p.scheme.lower() not in REMOTE_SCHEMES:\n    raise SystemExit(f\"scheme {p.scheme!r} unsupported; use http(s)://, file://, builtin://, or a path\")","typeGuard":"def is_supported_catalog_url(url: str) -> bool:\n    u = url.strip()\n    p = urlparse(u)\n    return \"://\" not in u or p.scheme.lower() in {\"http\", \"https\", \"file\", \"builtin\"}","tryCatchPattern":"try:\n    add_source(project_root, url, policy=policy, priority=50)\nexcept BundlerError as exc:\n    if \"Unsupported catalog url scheme\" in str(exc):\n        # swap git/ssh clone URLs for the raw https/file URL of the catalog JSON\n        raise\n    raise","preventionTips":["Catalogs are fetched as files — always point at the raw JSON URL, never a git clone URL","Windows drive paths (C:\\…) are fine; only x:// schemes are scheme-checked"],"tags":["bundler","catalog","url","scheme","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}