github/spec-kit · error · BundlerError

Catalog entry '{resolved.entry.id}' has a non-HTTP(S) downlo

Error message

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>'.

What it means

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.

Source

Thrown at src/specify_cli/commands/bundle/__init__.py:876

    # makes urlparse raise ValueError. Surface it as the documented
    # BundlerError, like the sibling ``_validate_remote_url``, rather than
    # leaking a raw ValueError past the callers, which only catch BundlerError.
    try:
        parsed = urlparse(url)
    except ValueError:
        raise BundlerError(
            f"Catalog entry '{resolved.entry.id}' has a malformed download_url: {url}"
        ) from None
    scheme = parsed.scheme.lower()

    # ``file://`` URLs and bare filesystem paths (including Windows drive paths
    # like ``C:\bundle.yml``, which urlparse reads as a single-letter scheme)
    # are not valid catalog download URLs. Catalog URLs are HTTPS-only across
    # every catalog system; installing from disk is done by passing the path
    # positionally, which never reaches URL resolution. Give an actionable
    # error rather than accepting a scheme the rest of the codebase rejects.
    if scheme in ("", "file") or re.match(r"^[A-Za-z]:[\\/]", url):
        raise BundlerError(
            f"Catalog entry '{resolved.entry.id}' has a non-HTTP(S) download_url "
            f"({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>'."
        )

    # Validate the scheme/host *before* the offline gate so an invalid or
    # non-HTTPS download_url reports the real problem in every mode, rather
    # than a misleading "Network access disabled" under --offline.
    # (_download_remote_manifest re-checks this, but only once network access
    # is permitted.) HTTPS-only, http allowed for localhost.
    _require_https(f"bundle '{resolved.entry.id}'", url)

    if offline:
        raise BundlerError(
            f"Network access disabled; cannot download bundle '{resolved.entry.id}' "

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Host the artifact over HTTPS and set download_url to the https:// URL.
  2. To install from local disk, do not fix the catalog — pass the path positionally: specify bundle install <path-to-bundle.yml | bundle-dir | .zip>.
  3. For local HTTP testing, use http://localhost/... which the localhost exception permits.

Example fix

# before (catalog entry)
"download_url": "file:///srv/bundles/my-bundle.zip"

# after
"download_url": "https://bundles.example.com/my-bundle.zip"
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse
import re

url = resolved.entry.download_url
parsed = urlparse(url)
scheme = parsed.scheme.lower()
if scheme in ("", "file") or re.match(r"^[A-Za-z]:[\\/]", url):
    raise SystemExit("Catalog download_url must be https:// (http for localhost); use a path argument for disk installs")

Type guard

def is_https_catalog_url(url: str) -> bool:
    from urllib.parse import urlparse
    import re
    if re.match(r"^[A-Za-z]:[\\/]", url):
        return False
    scheme = urlparse(url).scheme.lower()
    return scheme == "https" or (scheme == "http" and "localhost" in urlparse(url).netloc)

Try / catch

try:
    manifest = _download_manifest(resolved, offline=offline)
except BundlerError as exc:
    if "non-HTTP(S) download_url" in str(exc):
        # host the artifact over HTTPS, or install locally via positional path
        ...

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/91a9795a41246983. Report an issue: GitHub.