github/spec-kit · error · ExtensionError

Invalid JSON in catalog

Error message

Invalid JSON in catalog: {e}

What it means

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.

Solutions

  1. Inspect the actual body: `curl -s <catalog_url> | head -c 400`
  2. Use the raw JSON endpoint rather than a UI/auth-wrapped page
  3. Fix credentials or headers the catalog server requires so it serves JSON
  4. Re-validate and re-upload edited catalog files after `python -m json.tool` passes
Defensive patterns

Strategy: validation

Validate before calling

import json, urllib.request

def probe_catalog_json(url: str) -> bool:
    with urllib.request.urlopen(url, timeout=10) as r:
        head = r.read(1)
        if not head:
            return False
        r2 = urllib.request.urlopen(url, timeout=10)
        try:
            json.loads(r2.read())
            return True
        except json.JSONDecodeError:
            return False

Try / catch

from specify_cli.extensions import ExtensionError

try:
    results = catalog.search(query='ci')
except ExtensionError as e:
    if 'Invalid JSON in catalog' in str(e):
        # body wasn't JSON: inspect with curl, fix auth/raw-URL, then retry

Prevention

When it happens

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

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

Understand the failure class

Related errors


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

Appendix: source

Thrown at src/specify_cli/extensions/__init__.py:4121

                )

                # Save cache metadata
                metadata = {
                    "cached_at": datetime.now(timezone.utc).isoformat(),
                    "catalog_url": catalog_url,
                }
                self.cache_metadata_file.write_text(
                    json.dumps(metadata, indent=2), encoding="utf-8"
                )
            except OSError:
                pass  # Cache is best-effort; proceed with fetched data

            return catalog_data

        except urllib.error.URLError as e:
            raise ExtensionError(f"Failed to fetch catalog from {catalog_url}: {e}")
        except json.JSONDecodeError as e:
            raise ExtensionError(f"Invalid JSON in catalog: {e}")

    def search(
        self,
        query: Optional[str] = None,
        tag: Optional[str] = None,
        author: Optional[str] = None,
        verified_only: bool = False,
    ) -> List[Dict[str, Any]]:
        """Search catalog for extensions across all active catalogs.

        Args:
            query: Search query (searches name, description, tags)
            tag: Filter by specific tag
            author: Filter by author name
            verified_only: If True, show only verified extensions

        Returns:
            List of matching extension metadata, each annotated with

View on GitHub (pinned to bf88c9f9a8)