{"record":{"id":"b0c46648de7255d2","repo":"github/spec-kit","slug":"catalog-entry-resolved-entry-id-has-a-malforme","errorCode":null,"errorMessage":"Catalog entry '{resolved.entry.id}' has a malformed download_url: {url}","messagePattern":"Catalog entry '(.+?)' has a malformed download_url: (.+?)","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/commands/bundle/__init__.py","lineNumber":864,"sourceCode":"    ``.zip`` artifact also works), which :func:`_local_manifest_source` handles\n    before catalog resolution and which never touches ``download_url``.\n    \"\"\"\n    from urllib.parse import urlparse\n\n    url = resolved.entry.download_url\n    if not url:\n        raise BundlerError(\n            f\"Catalog entry '{resolved.entry.id}' has no download_url; cannot resolve \"\n            \"its manifest.\"\n        )\n    # A malformed authority (e.g. an unclosed IPv6 bracket ``https://[::1``)\n    # makes urlparse raise ValueError. Surface it as the documented\n    # BundlerError, like the sibling ``_validate_remote_url``, rather than\n    # leaking a raw ValueError past the callers, which only catch BundlerError.\n    try:\n        parsed = urlparse(url)\n    except ValueError:\n        raise BundlerError(\n            f\"Catalog entry '{resolved.entry.id}' has a malformed download_url: {url}\"\n        ) from None\n    scheme = parsed.scheme.lower()\n\n    # ``file://`` URLs and bare filesystem paths (including Windows drive paths\n    # like ``C:\\bundle.yml``, which urlparse reads as a single-letter scheme)\n    # are not valid catalog download URLs. Catalog URLs are HTTPS-only across\n    # every catalog system; installing from disk is done by passing the path\n    # positionally, which never reaches URL resolution. Give an actionable\n    # error rather than accepting a scheme the rest of the codebase rejects.\n    if scheme in (\"\", \"file\") or re.match(r\"^[A-Za-z]:[\\\\/]\", url):\n        raise BundlerError(\n            f\"Catalog entry '{resolved.entry.id}' has a non-HTTP(S) download_url \"\n            f\"({url}); catalog download URLs must be HTTPS (http for localhost) — \"\n            \"a file:// URL, a local filesystem path, or a scheme-less value \"\n            \"(e.g. 'example.com/bundle.zip') is not accepted. \"\n            \"To install a bundle from disk, pass the path directly: \"\n            \"'specify bundle install <path-to-bundle.yml | bundle-dir | .zip>'.\"","sourceCodeStart":846,"sourceCodeEnd":882,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/commands/bundle/__init__.py#L846-L882","documentation":"urlparse raised ValueError on the catalog entry's download_url — a malformed URL that Python cannot even parse, e.g. an unclosed IPv6 literal bracket ('https://[::1'). The CLI converts it to BundlerError (with `from None` to hide the noisy chain) so the contract 'callers only catch BundlerError' holds, instead of leaking a raw ValueError past the command handlers.","triggerScenarios":"A catalog entry with download_url like 'https://[::1' (unclosed bracket) or another urlparse-invalid authority; any install/update that must fetch that entry's manifest.","commonSituations":"Hand-edited catalog files with typos; templating pipelines truncating URLs; IPv6-local test registries written incorrectly.","solutions":["Correct the download_url in the catalog entry (close brackets, fix syntax) and re-cache.","Validate catalog URLs at publish time: python -c \"from urllib.parse import urlparse; urlparse(url)\".","Work around locally by installing from a path artifact."],"exampleFix":"# before (catalog entry)\n\"download_url\": \"https://[::1/bundle.zip\"\n\n# after\n\"download_url\": \"https://[::1]/bundle.zip\"","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\nurl = resolved.entry.download_url\ntry:\n    urlparse(url)\nexcept ValueError:\n    raise SystemExit(f\"Malformed catalog download_url: {url!r}\")","typeGuard":"def is_parseable_url(url: str) -> bool:\n    from urllib.parse import urlparse\n    try:\n        urlparse(url)\n        return True\n    except ValueError:\n        return False","tryCatchPattern":"try:\n    manifest = _download_manifest(resolved, offline=offline)\nexcept BundlerError as exc:\n    if \"malformed download_url\" in str(exc):\n        # fix the catalog entry URL; meanwhile install from a local path\n        ...","preventionTips":["Run urlparse() over catalog URLs when generating/publishing catalogs.","Watch for truncated IPv6 literals and templating artifacts in generated catalogs."],"tags":["bundler","catalog","url","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}