{"record":{"id":"7925f8c11be309df","repo":"odysseus-dev/odysseus","slug":"query-is-required","errorCode":null,"errorMessage":"query is required","messagePattern":"query is required","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"info","filePath":"routes/skills_routes.py","lineNumber":1656,"sourceCode":"    @router.delete(\"/{skill_id}\")\n    async def delete_skill(request: Request, skill_id: str):\n        user = _owner(request)\n        skills = skills_manager.load(owner=user)\n        match = next((s for s in skills if s.get(\"name\") == skill_id or s.get(\"id\") == skill_id), None)\n        if not match:\n            raise HTTPException(404, \"Skill not found\")\n        _verify_owner(match, user)\n        ok = skills_manager.delete_skill(match.get(\"name\"), owner=user)\n        if not ok:\n            raise HTTPException(404, \"Skill not found\")\n        return {\"ok\": True}\n\n    @router.post(\"/search\")\n    async def search_skills(request: Request):\n        body = await request.json()\n        query = body.get(\"query\", \"\")\n        if not query.strip():\n            raise HTTPException(400, \"query is required\")\n        user = _owner(request)\n        skills = skills_manager.load(owner=user)\n        results = skills_manager.get_relevant_skills(query, skills, max_items=10)\n        return {\"skills\": results, \"query\": query, \"count\": len(results)}\n\n    return router\n","sourceCodeStart":1638,"sourceCodeEnd":1663,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/skills_routes.py#L1638-L1663","documentation":"400 from POST /api/skills/search: body.get('query') is missing, None, or whitespace-only. The route requires a non-empty query string before running get_relevant_skills; an empty query has no ranking semantics, so it is rejected rather than returning the whole list.","triggerScenarios":"POST /search with {} , {\"query\": null}, {\"query\": \"   \"}, or the field under a different key ('q', 'text'); client sending an empty search box submission.","commonSituations":"Search form submitted before typing; template literal producing undefined serialized as null; API consumer assuming GET-style ?q= parameter instead of the JSON body.","solutions":["Send a non-empty JSON body: {\"query\": \"some text\"}.","Guard client-side: disable submit while the query is blank.","If listing all skills is the intent, use GET /api/skills instead of an empty search."],"exampleFix":"# before\nclient.post(\"/api/skills/search\", json={\"query\": \"\"})  # 400\n\n# after\nq = input_text.strip()\nif not q:\n    return client.get(\"/api/skills\").json()  # list-all fallback client-side\nclient.post(\"/api/skills/search\", json={\"query\": q})","handlingStrategy":"validation","validationCode":"query = (body.get(\"query\") or \"\").strip() if isinstance(body, dict) else \"\"\nif not query:\n    results = client.get(f\"{base}/api/skills\").json()  # list-all instead of empty search\nelse:\n    results = client.post(f\"{base}/api/skills/search\", json={\"query\": query}).json()","typeGuard":"def is_valid_search_body(body: dict) -> bool:\n    q = body.get(\"query\")\n    return isinstance(q, str) and bool(q.strip())","tryCatchPattern":null,"preventionTips":["Disable search submit while the query box is empty.","Use the exact JSON key 'query' (not 'q').","Use GET /api/skills when the intent is listing, not searching."],"tags":["skills","http-400","search","validation","request-body"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}