{"record":{"id":"0106a3cac34c7266","repo":"github/spec-kit","slug":"providers-i-each-host-must-be-a-non-empty-stri","errorCode":null,"errorMessage":"providers[{i}]: each host must be a non-empty string","messagePattern":"providers\\[(.+?)\\]: each host must be a non-empty string","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/authentication/config.py","lineNumber":133,"sourceCode":"    raw = json.loads(config_path.read_text(encoding=\"utf-8\"))\n\n    if not isinstance(raw, dict):\n        raise ValueError(f\"auth.json must be a JSON object, got {type(raw).__name__}\")\n\n    providers_raw = raw.get(\"providers\")\n    if not isinstance(providers_raw, list):\n        raise ValueError(\"auth.json must contain a 'providers' array\")\n\n    entries: list[AuthConfigEntry] = []\n    for i, entry_raw in enumerate(providers_raw):\n        if not isinstance(entry_raw, dict):\n            raise ValueError(f\"providers[{i}]: must be a JSON object\")\n\n        hosts = entry_raw.get(\"hosts\")\n        if not isinstance(hosts, list) or not hosts:\n            raise ValueError(f\"providers[{i}]: 'hosts' must be a non-empty array\")\n        if not all(isinstance(h, str) and h.strip() for h in hosts):\n            raise ValueError(f\"providers[{i}]: each host must be a non-empty string\")\n        # Normalize hosts: strip whitespace and lowercase\n        hosts = [h.strip().lower() for h in hosts]\n        # Reject dangerous wildcard forms (e.g. *github.com matches github.com.evil.com)\n        for h in hosts:\n            if not _is_valid_host_pattern(h):\n                raise ValueError(\n                    f\"providers[{i}]: invalid host pattern {h!r}. \"\n                    \"Only exact hostnames or '*.suffix' forms are allowed \"\n                    \"(e.g. 'github.com' or '*.visualstudio.com').\"\n                )\n\n        provider = entry_raw.get(\"provider\", \"\")\n        if not isinstance(provider, str) or not provider:\n            raise ValueError(f\"providers[{i}]: 'provider' must be a non-empty string\")\n\n        auth = entry_raw.get(\"auth\", \"\")\n        if not isinstance(auth, str) or not auth:\n            raise ValueError(f\"providers[{i}]: 'auth' must be a non-empty string\")","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/authentication/config.py#L115-L151","documentation":"Every element of a provider's hosts array must be a non-empty string after stripping (h.strip() must be truthy). Non-string elements (numbers, null, objects) or whitespace-only/empty strings raise ValueError('providers[i]: each host must be a non-empty string'). Hosts are then normalized to lowercase and validated against the wildcard pattern policy.","triggerScenarios":"\"hosts\": [\"\"] or [\"   \"]; hosts: [null] from a template variable that expanded to nothing; hosts: [8080] or a port number accidentally placed where the hostname belongs; trailing-comma artifacts producing empty strings.","commonSituations":"Templated auth.json where ${HOST} is unset, yielding an empty string; configs edited to blank out a host instead of removing it; mixing 'hostname:port' values that a downstream transform split into non-strings.","solutions":["Remove empty/whitespace entries from the hosts array, keeping only real hostnames.","In templated configs, default the variable or skip emitting the provider when the host is unset.","Validate with jq: jq '.providers | map(.hosts | map(select((type!=\"string\") or (.|trim==\"\"))))' auth.json should yield only empty arrays."],"exampleFix":"// before\n{ \"hosts\": [\"\", \"github.com\"] }\n// after\n{ \"hosts\": [\"github.com\"] }","handlingStrategy":"validation","validationCode":"raw = json.loads(Path(\"auth.json\").read_text(encoding=\"utf-8\"))\nbad = [\n    (i, h) for i, e in enumerate(raw.get(\"providers\", []))\n    for h in e.get(\"hosts\", [])\n    if not isinstance(h, str) or not h.strip()\n]\nif bad:\n    raise SystemExit(f\"hosts must be non-empty strings; bad values: {bad}\")","typeGuard":"from typing import Any\n\ndef hosts_are_clean_strings(entry: Any) -> bool:\n    \"\"\"True when every host in the entry is a non-blank string.\"\"\"\n    hosts = entry.get(\"hosts\", []) if isinstance(entry, dict) else []\n    return all(isinstance(h, str) and h.strip() for h in hosts)","tryCatchPattern":"try:\n    entries = load_auth_config(path)\nexcept ValueError as exc:\n    if \"each host must be a non-empty string\" in str(exc):\n        raise SystemExit(f\"remove blank/non-string hosts: {exc}\") from exc\n    raise","preventionTips":["Hostnames only — no ports, schemes, or null placeholders in the hosts array.","When templating auth.json, skip emitting the provider if its host variable is empty.","Run jq '.providers[].hosts[] | select(. == \"\")' as a quick pre-flight check."],"tags":["authentication","json","configuration","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}