{"record":{"id":"e775d374ad6ff60f","repo":"anthropics/skills","slug":"expected-json-object-with-reviews-key","errorCode":null,"errorMessage":"Expected JSON object with 'reviews' key","messagePattern":"Expected JSON object with 'reviews' key","errorType":"http","errorClass":"ValueError","httpStatus":500,"severity":"warning","filePath":"skills/skill-creator/eval-viewer/generate_review.py","lineNumber":368,"sourceCode":"            data = b\"{}\"\n            if self.feedback_path.exists():\n                data = self.feedback_path.read_bytes()\n            self.send_response(200)\n            self.send_header(\"Content-Type\", \"application/json\")\n            self.send_header(\"Content-Length\", str(len(data)))\n            self.end_headers()\n            self.wfile.write(data)\n        else:\n            self.send_error(404)\n\n    def do_POST(self) -> None:\n        if self.path == \"/api/feedback\":\n            length = int(self.headers.get(\"Content-Length\", 0))\n            body = self.rfile.read(length)\n            try:\n                data = json.loads(body)\n                if not isinstance(data, dict) or \"reviews\" not in data:\n                    raise ValueError(\"Expected JSON object with 'reviews' key\")\n                self.feedback_path.write_text(json.dumps(data, indent=2) + \"\\n\")\n                resp = b'{\"ok\":true}'\n                self.send_response(200)\n            except (json.JSONDecodeError, OSError, ValueError) as e:\n                resp = json.dumps({\"error\": str(e)}).encode()\n                self.send_response(500)\n            self.send_header(\"Content-Type\", \"application/json\")\n            self.send_header(\"Content-Length\", str(len(resp)))\n            self.end_headers()\n            self.wfile.write(resp)\n        else:\n            self.send_error(404)\n\n    def log_message(self, format: str, *args: object) -> None:\n        # Suppress request logging to keep terminal clean\n        pass\n\n","sourceCodeStart":350,"sourceCodeEnd":386,"githubUrl":"https://github.com/anthropics/skills/blob/f6656c1256d5a8adfa37db9110046ef20bac644c/skills/skill-creator/eval-viewer/generate_review.py#L350-L386","documentation":"Raised inside the eval-viewer's POST /api/feedback handler when the request body parses as JSON but is not an object containing a 'reviews' key. The ValueError is caught and returned to the browser as an HTTP 500 with the message in the JSON error field — so the client sees 500 even though it is really a 400-class bad-request problem.","triggerScenarios":"POSTing to /api/feedback with a JSON array, a bare string/number, or an object without 'reviews' (e.g. {\"feedback\": ...}); sending an empty body when Content-Length defaults to 0 (json.loads(b'') raises JSONDecodeError into the same 500 path); malformed JSON from a hand-rolled fetch() call.","commonSituations":"Custom tooling or curl experiments that guess the payload shape instead of reusing the viewer's own save routine; proxies stripping the request body; the client sending form-encoded data where JSON is expected.","solutions":["POST a JSON object of the shape {\"reviews\": [...]} with Content-Type: application/json","Inspect the response body's \"error\" field — it echoes the exact parse/validation failure","If you control the server, mirror the payload the browser UI sends (capture it in devtools) before writing your own client"],"exampleFix":"// before\nawait fetch('/api/feedback', { method: 'POST', body: JSON.stringify(items) });\n// after\nawait fetch('/api/feedback', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ reviews: items }),\n});","handlingStrategy":"validation","validationCode":"def is_valid_feedback_payload(raw: bytes) -> bool:\n    try:\n        data = json.loads(raw)\n    except json.JSONDecodeError:\n        return False\n    return isinstance(data, dict) and \"reviews\" in data and isinstance(data[\"reviews\"], list)","typeGuard":"def is_feedback_obj(v) -> bool:\n    return isinstance(v, dict) and isinstance(v.get(\"reviews\"), list)","tryCatchPattern":"resp = requests.post(url + \"/api/feedback\", json={\"reviews\": reviews})\nif resp.status_code != 200:\n    raise RuntimeError(f\"feedback rejected: {resp.json().get('error')}\")","preventionTips":["Always wrap the payload as {\"reviews\": [...]}, never a bare list","Send Content-Type: application/json and a non-empty body","Record one real browser request in devtools and reuse its exact shape"],"tags":["json","http","api-contract","validation"],"backgroundTag":null,"analyzedSha":"f6656c1256d5a8adfa37db9110046ef20bac644c","analyzedAt":"2026-08-14T16:09:17.493Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}