{"record":{"id":"ca8d14bab4c24229","repo":"github/spec-kit","slug":"a-catalog-url-is-required","errorCode":null,"errorMessage":"A catalog url is required.","messagePattern":"A catalog url is required\\.","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/bundler/commands_impl/catalog_config.py","lineNumber":145,"sourceCode":"        host = parsed.hostname or \"\"\n        path_stem = Path(parsed.path).stem if parsed.path else \"\"\n        parts = [p for p in (_slug(host), _slug(path_stem)) if p]\n        return \"-\".join(parts) or \"catalog\"\n    stem = Path(parsed.path or url).stem\n    return _slug(stem) or \"catalog\"\n\n\ndef add_source(\n    project_root: Path,\n    url: str,\n    *,\n    policy: str,\n    priority: int,\n    source_id: str | None = None,\n) -> CatalogSource:\n    url = url.strip()\n    if not url:\n        raise BundlerError(\"A catalog url is required.\")\n    try:\n        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.","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/commands_impl/catalog_config.py#L127-L163","documentation":"Raised by `add_source()` when the `url` argument is empty after stripping. Adding a catalog source requires a URL pointing at the catalog (http(s)://, file://, builtin://, or a local path); a blank one cannot proceed.","triggerScenarios":"Calling the catalog-add API with `url=\"\"`, `url=\"   \"`, or a variable that expanded to nothing.","commonSituations":"Shell script passing an unset `$CATALOG_URL` without `set -u`/defaults; CLI invocation missing the positional URL argument in a wrapper; CI variable name typo yielding an empty string.","solutions":["Supply a non-empty catalog URL (https://…, file://…, builtin://…, or a local path)","Guard the caller: fail early if the URL variable is empty before invoking add","Use shell defaults like `${CATALOG_URL:?CATALOG_URL required}`"],"exampleFix":"# before\nspecify bundle catalog add \"$MAYBE_UNSET_URL\" --policy auto\n\n# after\nspecify bundle catalog add \"${CATALOG_URL:?CATALOG_URL required}\" --policy auto","handlingStrategy":"validation","validationCode":"url = (url or \"\").strip()\nif not url:\n    raise SystemExit(\"catalog URL is required (https://…, file://…, builtin://…, or local path)\")","typeGuard":"def is_nonempty_url(url: object) -> bool:\n    return isinstance(url, str) and bool(url.strip())","tryCatchPattern":"try:\n    add_source(project_root, url, policy=policy, priority=50)\nexcept BundlerError as exc:\n    if \"A catalog url is required\" in str(exc):\n        # re-prompt for the URL; do not silently pass \"\"\n        raise\n    raise","preventionTips":["Use shell guards like ${CATALOG_URL:?required} so empty variables abort early","Fail fast on empty inputs in wrappers instead of forwarding them"],"tags":["bundler","catalog","validation","cli"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}