HKUDS/DeepTutor · error · HubError

{self.name}: skill detail returned invalid JSON

Error message

{self.name}: skill detail returned invalid JSON

What it means

Raised by HubProvider.detail when GET /skills/{slug} returns a body that fails JSON parsing. The body is HTML/plain-text/empty, so metadata and the SKILL.md body cannot be extracted.

Source

Thrown at deeptutor/services/skill/hub.py:430

                if isinstance(payload.get(key), list):
                    rows = payload[key]
                    break
        if rows is None:
            raise HubError(f"{self.name}: unrecognised catalog response shape")
        out: list[dict[str, Any]] = []
        for row in rows:
            if isinstance(row, dict):
                listing = self._listing_from_row(row)
                if listing is not None:
                    out.append(listing)
        return out

    def detail(self, slug: str) -> dict[str, Any]:
        """Full metadata + SKILL.md body for one hub skill (browser detail view)."""
        try:
            payload = self._get(f"/skills/{slug}").json()
        except ValueError as exc:
            raise HubError(f"{self.name}: skill detail returned invalid JSON") from exc
        if not isinstance(payload, dict):
            raise HubError(f"{self.name}: unrecognised skill detail shape")
        skill = payload.get("skill") if isinstance(payload.get("skill"), dict) else payload
        listing = self._listing_from_row(skill) or {"slug": slug, "name": slug}
        # Owner sometimes lives at the envelope top level, not inside ``skill``.
        if not listing.get("owner") and isinstance(payload.get("owner"), dict):
            owner = payload["owner"]
            listing["owner"] = str(owner.get("displayName") or owner.get("handle") or "")
            listing["owner_url"] = str(owner.get("htmlUrl") or "")
        dist = payload.get("distTags") if isinstance(payload.get("distTags"), dict) else {}
        listing["version"] = str(dist.get("latest") or listing.get("version") or "")
        listing["content"] = str(skill.get("description") or "")
        # EduHub's ``tags`` field is a dist-tags map; the topical labels live in
        # ``keywords``. Fall back to a list-shaped ``tags`` for other hubs.
        keywords = skill.get("keywords")
        if not isinstance(keywords, list):
            keywords = skill.get("tags") if isinstance(skill.get("tags"), list) else []
        listing["tags"] = [str(t) for t in keywords if str(t).strip()]

View on GitHub (pinned to 3e82f13042)

Solutions

  1. curl -i $BASE_URL/skills/<slug> and inspect status/content-type
  2. Verify the slug exists via catalog/search first
  3. Correct base_url to the API host
  4. HTML-encode or validate the slug before calling detail
Defensive patterns

Strategy: try-catch

Validate before calling

resp = httpx.get(f"{BASE}/skills/{slug}")
ct = resp.headers.get("content-type", "")
if resp.status_code != 200 or "json" not in ct:
    skip_detail(slug)  # don't call detail()

Try / catch

try:
    d = hub.detail(slug)
except HubError as e:
    if "invalid JSON" in str(e):
        log.warning("detail unavailable for %s", slug); d = None
    else: raise

Prevention

When it happens

Trigger: Calling detail('slug') when the hub's skill-detail endpoint returns an HTML 404/500 page, an empty body, or a text error — e.g. a slug route not implemented by the deployed hub version.

Common situations: Slug-specific routes missing on older hub versions; proxy error pages; base_url pointing at the web UI rather than the API; slug containing characters that trigger a gateway rejection.

Understand the failure class

Related errors


AI-assisted analysis of HKUDS/DeepTutor@3e82f13042 (2026-08-27). Data as JSON: /api/errors/debca0ff61e61d7f. Report an issue: GitHub.