{"record":{"id":"c32a5cc3150c73ca","repo":"github/spec-kit","slug":"refusing-to-download-label-from-url-with-no-host","errorCode":null,"errorMessage":"Refusing to download {label} from URL with no host: {url}","messagePattern":"Refusing to download (.+?) from URL with no host: (.+?)","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/commands/bundle/__init__.py","lineNumber":927,"sourceCode":"    # urlparse / hostname access raise ValueError on a malformed authority;\n    # keep the documented BundlerError contract (older Pythons surface this via\n    # the .hostname access below rather than at the urlparse call).\n    try:\n        parsed = urlparse(url)\n        hostname = parsed.hostname\n        # Accessing ``port`` performs urllib's syntax/range validation.\n        _ = parsed.port\n    except ValueError:\n        raise BundlerError(\n            f\"Refusing to download {label}: URL is malformed: {url}\"\n        ) from None\n    is_localhost = hostname in (\"localhost\", \"127.0.0.1\", \"::1\")\n    if parsed.scheme != \"https\" and not (parsed.scheme == \"http\" and is_localhost):\n        raise BundlerError(\n            f\"Refusing to download {label} over non-HTTPS URL: {url}\"\n        )\n    if not parsed.hostname:\n        raise BundlerError(f\"Refusing to download {label} from URL with no host: {url}\")\n\n\ndef _download_remote_manifest(\n    entry_id: str,\n    url: str,\n    *,\n    expected_sha256: str | None = None,\n):\n    \"\"\"Fetch a remote bundle artifact over HTTPS and extract its manifest.\"\"\"\n    import io\n    import tempfile\n    from pathlib import PurePosixPath\n    from urllib.parse import urlparse as _urlparse\n\n    import yaml as _yaml\n\n    from ...authentication.http import github_provider_hosts, open_url\n    from ..._github_http import resolve_github_release_asset_api_url","sourceCodeStart":909,"sourceCodeEnd":945,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/commands/bundle/__init__.py#L909-L945","documentation":"After the scheme check passes, Spec Kit requires the download URL to contain a hostname. URLs such as `https:///bundle.zip` or `https://:8443/bundle.zip` have no authority and are rejected as BundlerError. This prevents an ambiguous, scheme-only URL from reaching the downloader.","triggerScenarios":"A catalog entry, redirect target, or final response URL uses HTTPS but omits or empties the host component, for example `https:///downloads/bundle.zip`.","commonSituations":"A catalog URL was assembled by string concatenation and the host variable was empty. Someone deleted the host while editing a URL, or a reverse proxy emitted a Location header without a host.","solutions":["Inspect the URL in the message and add the missing hostname, for example `https://example.com/downloads/bundle.zip`.","Check how the catalog download_url is generated if it is produced from variables or a template.","If the bad URL is a redirect/final URL, fix the server's Location header and retry."],"exampleFix":"# before\n\"download_url\": \"https:///bundles/my-bundle.zip\"\n\n# after\n\"download_url\": \"https://catalog.example.com/bundles/my-bundle.zip\"","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef url_has_https_host(url: str) -> bool:\n    try:\n        p = urlparse(url)\n        _ = p.port\n    except ValueError:\n        return False\n    return p.scheme == \"https\" and bool(p.hostname)","typeGuard":null,"tryCatchPattern":"except BundlerError as exc:\n    if \"URL with no host\" in str(exc):\n        reject_catalog_entry_with_empty_host()\n    else:\n        raise","preventionTips":["Assert both scheme and hostname in catalog linting.","Never build URLs by concatenating a possibly empty host variable.","Validate generated catalog JSON in CI before publishing."],"tags":["security","url","bundle","catalog","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}