{"record":{"id":"fc0aa1a3367831d5","repo":"github/spec-kit","slug":"failed-to-fetch-any-extension-catalog","errorCode":null,"errorMessage":"Failed to fetch any extension catalog","messagePattern":"Failed to fetch any extension catalog","errorType":"exception","errorClass":"ExtensionError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":3987,"sourceCode":"                # that ``catalog_data[\"extensions\"]`` is a mapping, but it\n                # does not (and should not) validate every entry shape there\n                # — one malformed entry shouldn't poison an otherwise valid\n                # catalog. Skip non-mapping entries here so a payload like\n                # ``{\"extensions\": {\"foo\": [], \"bar\": {...}}}`` still merges\n                # the valid entries without crashing on ``**ext_data``.\n                # Mirrors ``integrations/catalog.py:245``.\n                if not isinstance(ext_data, dict):\n                    continue\n                if ext_id not in merged:  # Higher-priority catalog wins\n                    merged[ext_id] = {\n                        **ext_data,\n                        \"id\": ext_id,\n                        \"_catalog_name\": catalog_entry.name,\n                        \"_install_allowed\": catalog_entry.install_allowed,\n                    }\n\n        if not any_success and active_catalogs:\n            raise ExtensionError(\"Failed to fetch any extension catalog\")\n\n        return list(merged.values())\n\n    def is_cache_valid(self) -> bool:\n        \"\"\"Check if cached catalog is still valid.\n\n        Returns ``False`` for any read/decoding failure on the metadata\n        file (missing fields, malformed JSON, permissions / disk errors,\n        wrong text encoding) so callers fall through to a network refetch\n        instead of crashing. Treating cache validity as best-effort\n        matches the contract used by the per-URL cache check below.\n\n        Returns:\n            True if cache exists and is within cache duration\n        \"\"\"\n        if not self.cache_file.exists() or not self.cache_metadata_file.exists():\n            return False\n","sourceCodeStart":3969,"sourceCodeEnd":4005,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L3969-L4005","documentation":"Raised by _get_merged_extensions when there is at least one active catalog configured but every single fetch attempt failed (any_success is false). This is the total-failure sentinel: individual catalog failures are normally tolerated and skipped, so seeing this means the entire catalog stack — custom and built-in — was unreachable or malformed in a way that produced no successful fetch.","triggerScenarios":"Running `specify extension list`/`search` (or anything calling _get_merged_extensions) while completely offline with the default catalog stack active, or with a custom SPECKIT_CATALOG_URL / catalog config where every configured URL fails.","commonSituations":"Air-gapped or CI environments with no network egress; SPECKIT_CATALOG_URL pointing at a dead server (it replaces all defaults, so its failure alone triggers this); system-level DNS outage.","solutions":["Restore network access or allow egress to the catalog hosts, then retry","If SPECKIT_CATALOG_URL is set, verify it: `echo $SPECKIT_CATALOG_URL` and `curl -f <url>`; unset it to fall back to defaults once reachable","Mirror the catalog locally and point the env var at http://localhost (localhost HTTP is permitted)","Check whether cached catalog data can bridge the outage — if a valid cache exists, investigate why the refetch path was forced (force_refresh)"],"exampleFix":"# before: env var forces a single dead catalog\nexport SPECKIT_CATALOG_URL=https://internal.example.invalid/catalog.json\n\n# after: local mirror while offline, or unset to use defaults\npython -m http.server 8080 --directory /path/to/mirror &\nexport SPECKIT_CATALOG_URL=http://localhost:8080/catalog.json","handlingStrategy":"fallback","validationCode":"import socket, urllib.parse\n\ndef can_reach_any(hosts: list[str]) -> bool:\n    for h in hosts:\n        try:\n            socket.create_connection((h, 443), timeout=3).close()\n            return True\n        except OSError:\n            continue\n    return False","typeGuard":null,"tryCatchPattern":"from specify_cli.extensions import ExtensionError\n\ntry:\n    merged = manager._get_merged_extensions()\nexcept ExtensionError as e:\n    if str(e) == 'Failed to fetch any extension catalog':\n        merged = []  # offline fallback: degrade to empty list with a warning\n        ...","preventionTips":["Ensure CI has egress to catalog hosts or serves a localhost mirror","If overriding with SPECKIT_CATALOG_URL, remember it replaces ALL defaults — its failure alone triggers total failure"],"tags":["extensions","catalog","network","offline"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}