github/spec-kit · error · ExtensionError

URL must use HTTPS (HTTP is only allowed for localhost)

Error message

URL must use HTTPS (HTTP is only allowed for localhost)

What it means

The URL installer's first gate: is_https_or_localhost_http(url) returned false, meaning the scheme is not HTTPS (and not http:// on a loopback host) — or the URL failed basic parse/shape checks. This is a deliberate security control: extension archives are executable content, so plaintext HTTP is only tolerated for localhost development.

Source

Thrown at src/specify_cli/extensions/_commands.py:123

    force: bool = False,
):
    """Download an archive from *url* and install it, reusing the hardened path.

    Shares the same download hardening as ``extension add --from``:
    HTTPS enforcement, the catalog's authenticated + redirect-guarded
    ``_open_url`` fetch, a bounded (50 MiB) response read, archive-format
    detection (ZIP or tar.gz/tgz), and a TOCTOU-safe transient download file
    consumed directly by ``install_from_zip``.

    Returns the installed manifest. Raises ``ExtensionError`` on any failure so
    callers can present a uniform message without a second downloader.
    """
    import urllib.error

    from . import ExtensionCatalog, ExtensionError

    if not is_https_or_localhost_http(url):
        raise ExtensionError(
            "URL must use HTTPS (HTTP is only allowed for localhost)"
        )

    download_dir = _validate_safe_cache_dir(project_root)
    archive_filename = f"extension-url-download-{uuid4().hex}.archive"
    # Only used for diagnostic messages: the real archive is a transient inode
    # (unlinked on POSIX, O_TEMPORARY on Windows) consumed via ``archive_file``
    # below, so this path is never opened again.
    archive_path = download_dir / archive_filename

    try:
        dl_catalog = ExtensionCatalog(project_root)
        download_url = url
        extra_headers = None
        resolved_url = dl_catalog._resolve_github_release_asset_api_url(download_url)
        if resolved_url:
            download_url = resolved_url
            extra_headers = {"Accept": "application/octet-stream"}

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Serve the archive over HTTPS (e.g. behind a TLS-terminating proxy or with a self-signed cert added to the trust store) and use the https:// URL
  2. For local development, keep HTTP but address it as localhost/127.0.0.1 (::1) so the loopback exemption applies
  3. For local files, skip the URL path entirely — install from the local archive path or directory
  4. Fix the typo'd scheme (http:// -> https://)

Example fix

# before
specify extension install http://internal-mirror:8080/my-ext.zip

# after (localhost exemption)
python -m http.server 8080 &
specify extension install http://localhost:8080/my-ext.zip
# or
specify extension install https://mirror.example.com/my-ext.zip
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse

def url_allowed_for_install(url: str) -> bool:
    p = urlparse(url)
    host = (p.hostname or '').lower()
    return p.scheme == 'https' or (
        p.scheme == 'http' and host in ('localhost', '127.0.0.1', '::1')
    )

assert url_allowed_for_install('https://example.com/ext.zip')

Type guard

def assert_installable_url(url: str) -> None:
    if not url_allowed_for_install(url):
        raise ValueError(
            'Extension URLs must be https:// (http:// only on localhost)'
        )

Prevention

When it happens

Trigger: Calling the install-from-URL path (`specify extension install <url>` or _commands installer) with http:// on a non-localhost host, or with a scheme like file:// or ftp://.

Common situations: Using http:// to dodge TLS errors on an internal mirror; copy-pasting a file:// path instead of a URL; dev server on 0.0.0.0 or a LAN IP over plain http; typo dropping the s in https.

Related errors


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