{"record":{"id":"91a9795a41246983","repo":"github/spec-kit","slug":"catalog-entry-resolved-entry-id-has-a-non-http","errorCode":null,"errorMessage":"Catalog entry '{resolved.entry.id}' has a non-HTTP(S) download_url ({url}); catalog download URLs must be HTTPS (http for localhost) — a file:// URL, a local filesystem path, or a scheme-less value (e.g. 'example.com/bundle.zip') is not accepted. To install a bundle from disk, pass the path directly: 'specify bundle install <path-to-bundle.yml | bundle-dir | .zip>'.","messagePattern":"Catalog entry '(.+?)' has a non-HTTP\\(S\\) download_url \\((.+?)\\); catalog download URLs must be HTTPS \\(http for localhost\\) — a file:// URL, a local filesystem path, or a scheme-less value \\(e\\.g\\. 'example\\.com/bundle\\.zip'\\) is not accepted\\. To install a bundle from disk, pass the path directly: 'specify bundle install <path-to-bundle\\.yml \\| bundle-dir \\| \\.zip>'\\.","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/commands/bundle/__init__.py","lineNumber":876,"sourceCode":"    # 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>'.\"\n        )\n\n    # Validate the scheme/host *before* the offline gate so an invalid or\n    # non-HTTPS download_url reports the real problem in every mode, rather\n    # than a misleading \"Network access disabled\" under --offline.\n    # (_download_remote_manifest re-checks this, but only once network access\n    # is permitted.) HTTPS-only, http allowed for localhost.\n    _require_https(f\"bundle '{resolved.entry.id}'\", url)\n\n    if offline:\n        raise BundlerError(\n            f\"Network access disabled; cannot download bundle '{resolved.entry.id}' \"","sourceCodeStart":858,"sourceCodeEnd":894,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/commands/bundle/__init__.py#L858-L894","documentation":"The download_url scheme check in _download_manifest: catalog URLs must be HTTPS (plain http only for localhost). This fires for scheme-less values ('example.com/bundle.zip'), file:// URLs, bare filesystem paths, and Windows drive paths ('C:\\bundle.yml' — urlparse sees a single-letter scheme), each rejected with a redirect telling the user how to install from disk instead. The check runs before the offline gate so the real problem is reported in every mode.","triggerScenarios":"A catalog entry whose download_url is 'file:///bundles/my-bundle.zip', '/home/me/bundle.zip', 'C:\\bundles\\bundle.zip', or 'example.com/bundle.zip'; any 'specify bundle install <catalog-id>' resolving to that entry.","commonSituations":"Private/internal catalogs written with LAN file shares; Windows-path entries; users copying a path into the catalog instead of hosting the artifact over HTTPS.","solutions":["Host the artifact over HTTPS and set download_url to the https:// URL.","To install from local disk, do not fix the catalog — pass the path positionally: specify bundle install <path-to-bundle.yml | bundle-dir | .zip>.","For local HTTP testing, use http://localhost/... which the localhost exception permits."],"exampleFix":"# before (catalog entry)\n\"download_url\": \"file:///srv/bundles/my-bundle.zip\"\n\n# after\n\"download_url\": \"https://bundles.example.com/my-bundle.zip\"","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\nimport re\n\nurl = resolved.entry.download_url\nparsed = urlparse(url)\nscheme = parsed.scheme.lower()\nif scheme in (\"\", \"file\") or re.match(r\"^[A-Za-z]:[\\\\/]\", url):\n    raise SystemExit(\"Catalog download_url must be https:// (http for localhost); use a path argument for disk installs\")","typeGuard":"def is_https_catalog_url(url: str) -> bool:\n    from urllib.parse import urlparse\n    import re\n    if re.match(r\"^[A-Za-z]:[\\\\/]\", url):\n        return False\n    scheme = urlparse(url).scheme.lower()\n    return scheme == \"https\" or (scheme == \"http\" and \"localhost\" in urlparse(url).netloc)","tryCatchPattern":"try:\n    manifest = _download_manifest(resolved, offline=offline)\nexcept BundlerError as exc:\n    if \"non-HTTP(S) download_url\" in str(exc):\n        # host the artifact over HTTPS, or install locally via positional path\n        ...","preventionTips":["Never put file:// paths or bare filesystem paths in catalog download_url fields.","Host bundle artifacts on an HTTPS endpoint before indexing them."],"tags":["bundler","catalog","url","security","https"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}