{"record":{"id":"528f91f0b640aa84","repo":"github/spec-kit","slug":"failed-to-fetch-catalog-from-entry-url-e","errorCode":null,"errorMessage":"Failed to fetch catalog from {entry.url}: {e}","messagePattern":"Failed to fetch catalog from (.+?): (.+?)","errorType":"exception","errorClass":"ExtensionError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":3924,"sourceCode":"                    json.dumps(catalog_data, indent=2), encoding=\"utf-8\"\n                )\n                cache_meta_file.write_text(\n                    json.dumps(\n                        {\n                            \"cached_at\": datetime.now(timezone.utc).isoformat(),\n                            \"catalog_url\": entry.url,\n                        },\n                        indent=2,\n                    ),\n                    encoding=\"utf-8\",\n                )\n            except OSError:\n                pass  # Cache is best-effort; proceed with fetched data\n\n            return catalog_data\n\n        except urllib.error.URLError as e:\n            raise ExtensionError(f\"Failed to fetch catalog from {entry.url}: {e}\")\n        except json.JSONDecodeError as e:\n            raise ExtensionError(f\"Invalid JSON in catalog from {entry.url}: {e}\")\n\n    def _get_merged_extensions(\n        self, force_refresh: bool = False\n    ) -> List[Dict[str, Any]]:\n        \"\"\"Fetch and merge extensions from all active catalogs.\n\n        Higher-priority (lower priority number) catalogs win on conflicts\n        (same extension id in two catalogs). Each extension dict is annotated with:\n          - _catalog_name: name of the source catalog\n          - _install_allowed: whether installation is allowed from this catalog\n\n        Catalogs that fail to fetch are skipped. Raises ExtensionError only if\n        ALL catalogs fail.\n\n        Args:\n            force_refresh: If True, bypass all caches","sourceCodeStart":3906,"sourceCodeEnd":3942,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L3906-L3942","documentation":"Raised during a multi-catalog fetch when urllib raises URLError while retrieving one catalog entry (entry.url) — DNS failure, refused connection, timeout, TLS error, or HTTP error surfaced as URLError. The error names the failing URL so the user can tell which catalog in the stacked config is unreachable.","triggerScenarios":"Any active catalog URL (project .specify/extension-catalogs.yml, user ~/.specify/extension-catalogs.yml, or built-in defaults) being unreachable: `specify extension list/search` or any catalog-consuming command while offline or behind a blocking proxy.","commonSituations":"Working offline or in a sandboxed CI with no egress; a self-hosted catalog server down; corporate MITM proxy rejecting TLS; typo'd hostname; IPv6-only misconfiguration. Note the fetch loop skips failed catalogs and only raises 'Failed to fetch any extension catalog' if all fail — this specific error propagates when the fetch result is consumed directly.","solutions":["Check connectivity to the exact URL in the message: `curl -v <entry.url>`","Fix or remove the broken catalog entry from .specify/extension-catalogs.yml or ~/.specify/extension-catalogs.yml","If behind a proxy, set HTTPS_PROXY; if TLS interception breaks it, install the proxy CA or bypass it","Work offline by ensuring at least the built-in default catalog is reachable, or point SPECKIT_CATALOG_URL at a local file/http://localhost mirror (localhost HTTP is allowed)"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"import urllib.request\n\ndef catalog_reachable(url: str) -> bool:\n    try:\n        with urllib.request.urlopen(url, timeout=5) as r:\n            return r.status == 200\n    except OSError:\n        return False","typeGuard":null,"tryCatchPattern":"from specify_cli.extensions import ExtensionError\n\ntry:\n    extensions = manager._get_merged_extensions()\nexcept ExtensionError as e:\n    if str(e).startswith('Failed to fetch catalog from'):\n        # one catalog in the stack failed; drop it from config and retry\n        ...","preventionTips":["Keep a locally mirrored catalog URL as a fallback entry in extension-catalogs.yml","Verify custom catalog URLs with curl before committing the config"],"tags":["extensions","catalog","network","urllib"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}