{"record":{"id":"a0e56454489a070a","repo":"github/spec-kit","slug":"catalog-url-must-be-a-valid-url-with-a-host","errorCode":null,"errorMessage":"Catalog URL must be a valid URL with a host.","messagePattern":"Catalog URL must be a valid URL with a host\\.","errorType":"validation","errorClass":"PresetValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/presets/__init__.py","lineNumber":4164,"sourceCode":"\n        try:\n            parsed = urlparse(url)\n            hostname = parsed.hostname\n        except ValueError:\n            raise PresetValidationError(f\"Catalog URL is malformed: {url}\") from None\n        is_localhost = hostname in (\"localhost\", \"127.0.0.1\", \"::1\")\n        if parsed.scheme != \"https\" and not (\n            parsed.scheme == \"http\" and is_localhost\n        ):\n            raise PresetValidationError(\n                f\"Catalog URL must use HTTPS (got {parsed.scheme}://). \"\n                \"HTTP is only allowed for localhost.\"\n            )\n        # Check hostname, not netloc: netloc is truthy for host-less URLs like\n        # \"https://:8080\" or \"https://user@\", so the host guarantee this error\n        # promises would not actually hold. hostname is None in those cases (#3209).\n        if not hostname:\n            raise PresetValidationError(\n                \"Catalog URL must be a valid URL with a host.\"\n            )\n\n    def _make_request(self, url: str):\n        \"\"\"Build a urllib Request, adding auth headers when a provider matches.\n\n        Delegates to :func:`specify_cli.authentication.http.build_request`.\n        \"\"\"\n        from specify_cli.authentication.http import build_request\n        return build_request(url)\n\n    def _open_url(\n        self,\n        url: str,\n        timeout: int = 10,\n        extra_headers: Optional[Dict[str, str]] = None,\n        redirect_validator=None,\n    ):","sourceCodeStart":4146,"sourceCodeEnd":4182,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/presets/__init__.py#L4146-L4182","documentation":"The catalog URL parsed successfully but has no hostname (parsed.hostname is None). The validator deliberately checks hostname rather than netloc because netloc is truthy for host-less URLs like `https://:8080` or `https://user@`; per issue #3209, only a real hostname guarantees the URL has a host to connect to.","triggerScenarios":"Catalog URLs such as `https://:8080/catalog.json`, `https://user@/catalog.json`, `https:///path/catalog.json` (empty host), or scheme-only strings like `https://` — urlparse succeeds but hostname is None.","commonSituations":"Env-var templating that drops the hostname (`https://${HOST}/...` with HOST unset), hand-editing preset-catalogs.yml and deleting the host, or URL-building code that concatenates an empty host.","solutions":["Include an explicit hostname: `https://catalog.example.com/catalog.json`.","Check interpolated values (env vars, config keys) are non-empty before building the URL.","Pre-validate with urllib.parse: assert urlparse(url).hostname is not None."],"exampleFix":"# before\n  - url: \"https://:8443/catalog.json\"\n\n# after\n  - url: \"https://catalog.example.com:8443/catalog.json\"","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\nassert urlparse(url).hostname, f\"catalog URL {url!r} has no host\"","typeGuard":"from urllib.parse import urlparse\ndef url_has_host(url: str) -> bool:\n    try:\n        return urlparse(url).hostname is not None\n    except ValueError:\n        return False","tryCatchPattern":"try:\n    manager.add_catalog(url)\nexcept PresetValidationError as e:\n    if \"valid URL with a host\" in str(e):\n        # re-check templated env vars; rebuild as scheme://host[:port]/path\n        ...","preventionTips":["Check hostname, not netloc — 'https://:8080' and 'https://user@' look truthy but have no host (#3209).","Assert interpolated env vars are non-empty before building URLs.","Test catalog URLs once in CI: urlparse(url).hostname is not None."],"tags":["preset","catalog","url","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}