github/spec-kit · error · PresetValidationError

Catalog URL must be a valid URL with a host.

Error message

Catalog URL must be a valid URL with a host.

What it means

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.

Source

Thrown at src/specify_cli/presets/__init__.py:4164

        try:
            parsed = urlparse(url)
            hostname = parsed.hostname
        except ValueError:
            raise PresetValidationError(f"Catalog URL is malformed: {url}") from None
        is_localhost = hostname in ("localhost", "127.0.0.1", "::1")
        if parsed.scheme != "https" and not (
            parsed.scheme == "http" and is_localhost
        ):
            raise PresetValidationError(
                f"Catalog URL must use HTTPS (got {parsed.scheme}://). "
                "HTTP is only allowed for localhost."
            )
        # Check hostname, not netloc: netloc is truthy for host-less URLs like
        # "https://:8080" or "https://user@", so the host guarantee this error
        # promises would not actually hold. hostname is None in those cases (#3209).
        if not hostname:
            raise PresetValidationError(
                "Catalog URL must be a valid URL with a host."
            )

    def _make_request(self, url: str):
        """Build a urllib Request, adding auth headers when a provider matches.

        Delegates to :func:`specify_cli.authentication.http.build_request`.
        """
        from specify_cli.authentication.http import build_request
        return build_request(url)

    def _open_url(
        self,
        url: str,
        timeout: int = 10,
        extra_headers: Optional[Dict[str, str]] = None,
        redirect_validator=None,
    ):

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Include an explicit hostname: `https://catalog.example.com/catalog.json`.
  2. Check interpolated values (env vars, config keys) are non-empty before building the URL.
  3. Pre-validate with urllib.parse: assert urlparse(url).hostname is not None.

Example fix

# before
  - url: "https://:8443/catalog.json"

# after
  - url: "https://catalog.example.com:8443/catalog.json"
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse
assert urlparse(url).hostname, f"catalog URL {url!r} has no host"

Type guard

from urllib.parse import urlparse
def url_has_host(url: str) -> bool:
    try:
        return urlparse(url).hostname is not None
    except ValueError:
        return False

Try / catch

try:
    manager.add_catalog(url)
except PresetValidationError as e:
    if "valid URL with a host" in str(e):
        # re-check templated env vars; rebuild as scheme://host[:port]/path
        ...

Prevention

When it happens

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

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

Related errors


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