anthropics/skills · warning · ValueError
Expected JSON object with 'reviews' key
Error message
Expected JSON object with 'reviews' key
What it means
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.
Source
Thrown at skills/skill-creator/eval-viewer/generate_review.py:368
data = b"{}"
if self.feedback_path.exists():
data = self.feedback_path.read_bytes()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
else:
self.send_error(404)
def do_POST(self) -> None:
if self.path == "/api/feedback":
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
try:
data = json.loads(body)
if not isinstance(data, dict) or "reviews" not in data:
raise ValueError("Expected JSON object with 'reviews' key")
self.feedback_path.write_text(json.dumps(data, indent=2) + "\n")
resp = b'{"ok":true}'
self.send_response(200)
except (json.JSONDecodeError, OSError, ValueError) as e:
resp = json.dumps({"error": str(e)}).encode()
self.send_response(500)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(resp)))
self.end_headers()
self.wfile.write(resp)
else:
self.send_error(404)
def log_message(self, format: str, *args: object) -> None:
# Suppress request logging to keep terminal clean
pass
View on GitHub (pinned to f6656c1256)
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
Example fix
// before
await fetch('/api/feedback', { method: 'POST', body: JSON.stringify(items) });
// after
await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reviews: items }),
}); Defensive patterns
Strategy: validation
Validate before calling
def is_valid_feedback_payload(raw: bytes) -> bool:
try:
data = json.loads(raw)
except json.JSONDecodeError:
return False
return isinstance(data, dict) and "reviews" in data and isinstance(data["reviews"], list) Type guard
def is_feedback_obj(v) -> bool:
return isinstance(v, dict) and isinstance(v.get("reviews"), list) Try / catch
resp = requests.post(url + "/api/feedback", json={"reviews": reviews})
if resp.status_code != 200:
raise RuntimeError(f"feedback rejected: {resp.json().get('error')}") Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- URL is required for http transport
- {word} not found (not an unpacked .docx?)
- parent comment {parent_id} not found
- relationship target is not a POSIX part name: {target!r}
- relationship target resolves to nothing: {target!r}
AI-assisted analysis of anthropics/skills@f6656c1256 (2026-08-14).
Data as JSON: /api/errors/e775d374ad6ff60f.
Report an issue: GitHub.