{"record":{"id":"6da9b5b32ea86bd3","repo":"github/spec-kit","slug":"invalid-json-in-catalog-e","errorCode":null,"errorMessage":"Invalid JSON in catalog: {e}","messagePattern":"Invalid JSON in catalog: (.+?)","errorType":"exception","errorClass":"ExtensionError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":4121,"sourceCode":"                )\n\n                # Save cache metadata\n                metadata = {\n                    \"cached_at\": datetime.now(timezone.utc).isoformat(),\n                    \"catalog_url\": catalog_url,\n                }\n                self.cache_metadata_file.write_text(\n                    json.dumps(metadata, indent=2), 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 {catalog_url}: {e}\")\n        except json.JSONDecodeError as e:\n            raise ExtensionError(f\"Invalid JSON in catalog: {e}\")\n\n    def search(\n        self,\n        query: Optional[str] = None,\n        tag: Optional[str] = None,\n        author: Optional[str] = None,\n        verified_only: bool = False,\n    ) -> List[Dict[str, Any]]:\n        \"\"\"Search catalog for extensions across all active catalogs.\n\n        Args:\n            query: Search query (searches name, description, tags)\n            tag: Filter by specific tag\n            author: Filter by author name\n            verified_only: If True, show only verified extensions\n\n        Returns:\n            List of matching extension metadata, each annotated with","sourceCodeStart":4103,"sourceCodeEnd":4139,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L4103-L4139","documentation":"Companion to the URLError case on the single-catalog path: the HTTP fetch succeeded but json parsing of the response body raised JSONDecodeError, so the catalog is declared invalid JSON with the parse error included.","triggerScenarios":"catalog_url returns non-JSON bytes: an HTML error/login page with 200 status, an empty body, truncated output from a flaky proxy, or JSON with a syntax error.","commonSituations":"Auth-protected catalog returning an HTML login page; object-storage URL serving an XML error document; hand-edited catalog uploaded with a trailing comma; CDN serving a partial response on timeout.","solutions":["Inspect the actual body: `curl -s <catalog_url> | head -c 400`","Use the raw JSON endpoint rather than a UI/auth-wrapped page","Fix credentials or headers the catalog server requires so it serves JSON","Re-validate and re-upload edited catalog files after `python -m json.tool` passes"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import json, urllib.request\n\ndef probe_catalog_json(url: str) -> bool:\n    with urllib.request.urlopen(url, timeout=10) as r:\n        head = r.read(1)\n        if not head:\n            return False\n        r2 = urllib.request.urlopen(url, timeout=10)\n        try:\n            json.loads(r2.read())\n            return True\n        except json.JSONDecodeError:\n            return False","typeGuard":null,"tryCatchPattern":"from specify_cli.extensions import ExtensionError\n\ntry:\n    results = catalog.search(query='ci')\nexcept ExtensionError as e:\n    if 'Invalid JSON in catalog' in str(e):\n        # body wasn't JSON: inspect with curl, fix auth/raw-URL, then retry","preventionTips":["Smoke-test catalog URLs (status 200 + parses as JSON) in a pre-deploy step","Prefer raw file hosts for catalog JSON to avoid HTML envelopes"],"tags":["extensions","catalog","json","network"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}