{"record":{"id":"8e46180649c88f5f","repo":"HKUDS/DeepTutor","slug":"self-name-unrecognised-search-response-shape","errorCode":null,"errorMessage":"{self.name}: unrecognised search response shape","messagePattern":"(.+?): unrecognised search response shape","errorType":"http","errorClass":"HubError","httpStatus":null,"severity":"error","filePath":"deeptutor/services/skill/hub.py","lineNumber":301,"sourceCode":"            raise HubError(\n                f\"{self.name}: HTTP {response.status_code} for {path}: {response.text[:200]}\"\n            )\n        return response\n\n    def search(self, query: str, *, limit: int = 10) -> list[HubSkillRef]:\n        response = self._get(\"/search\", q=query, limit=limit)\n        try:\n            payload = response.json()\n        except ValueError as exc:\n            raise HubError(f\"{self.name}: search returned invalid JSON\") from exc\n        rows = payload if isinstance(payload, list) else None\n        if rows is None and isinstance(payload, dict):\n            for key in (\"results\", \"items\", \"skills\"):\n                if isinstance(payload.get(key), list):\n                    rows = payload[key]\n                    break\n        if rows is None:\n            raise HubError(f\"{self.name}: unrecognised search response shape\")\n        refs: list[HubSkillRef] = []\n        for row in rows:\n            if not isinstance(row, dict):\n                continue\n            slug = str(row.get(\"slug\") or \"\").strip()\n            if not slug:\n                continue\n            refs.append(\n                HubSkillRef(\n                    hub=self.name,\n                    slug=slug,\n                    display_name=str(row.get(\"displayName\") or row.get(\"name\") or slug),\n                    summary=str(row.get(\"summary\") or row.get(\"description\") or \"\"),\n                    version=str(row.get(\"version\") or \"\"),\n                )\n            )\n        return refs\n","sourceCodeStart":283,"sourceCodeEnd":319,"githubUrl":"https://github.com/HKUDS/DeepTutor/blob/3e82f130422a813cdd73c10b21a44e9325f5821a/deeptutor/services/skill/hub.py#L283-L319","documentation":"Raised by HubProvider.search when the hub's /search endpoint returns a payload that is neither a JSON list nor a dict containing a 'results', 'items', or 'skills' list. The client cannot locate any rows to normalise into HubSkillRef objects, so it aborts rather than returning misleading empty results.","triggerScenarios":"Calling hub.search(query) (or `skill search`) against a ClawHub-like provider whose /search response is a bare dict like {\"error\": ...}, a scalar, or an envelope using an unexpected key (e.g. {'data': {'hits': [...]}} with double nesting).","commonSituations":"Hub API version change that altered the response envelope; pointing base_url at a different service or an HTML error page that parsed as a scalar; a proxy returning {\"detail\": \"...\"} on auth failure.","solutions":["Inspect the raw response: curl $BASE_URL/search?q=test and check the envelope key holding the list","If the list sits under a different key, ensure the hub returns one of results/items/skills at the top level (or a bare array)","Verify base_url in settings/skill_hubs.json points at a compatible hub API version","If the hub genuinely cannot conform, wrap it with a CommandProvider instead"],"exampleFix":"// settings/skill_hubs.json (hub must return one of these shapes)\n// before: {\"data\": {\"results\": [...]}}\n// after:  {\"results\": [...]}","handlingStrategy":"validation","validationCode":"import httpx\nresp = httpx.get(f\"{BASE}/search\", params={\"q\": q})\npayload = resp.json()\nassert isinstance(payload, list) or any(isinstance(payload.get(k), list) for k in (\"results\",\"items\",\"skills\") if isinstance(payload, dict)), \"envelope not supported\"","typeGuard":"def has_supported_rows(p: object) -> bool:\n    if isinstance(p, list):\n        return True\n    return isinstance(p, dict) and any(isinstance(p.get(k), list) for k in (\"results\", \"items\", \"skills\"))","tryCatchPattern":"try:\n    refs = hub.search(q)\nexcept HubError as e:\n    if \"unrecognised search response shape\" in str(e):\n        return []  # degrade gracefully\n    raise","preventionTips":["Pin the hub API version you develop against","Add a smoke check of /search envelope at startup in dev/test"],"tags":["hub","search","response-shape","api-contract"],"backgroundTag":"unexpected-response-schema","analyzedSha":"3e82f130422a813cdd73c10b21a44e9325f5821a","analyzedAt":"2026-08-27T06:57:25.364Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}