odysseus-dev/odysseus · warning · HTTPException
winner must be 'left', 'right', or 'tie'
Error message
winner must be 'left', 'right', or 'tie'
What it means
Raised by the vote endpoint when the winner form field is not exactly 'left', 'right', or 'tie'. The comparison is case-sensitive and takes no other values; anything else (including 'a'/'b', 'Left', 'TIE') is rejected after the existence/ownership checks pass.
Source
Thrown at routes/compare/compare_routes.py:266
if not comp:
raise HTTPException(404, "Comparison not found")
# SECURITY: strict ownership — null-owner Comparisons were
# accessible to every user.
if user and comp.owner != user:
raise HTTPException(404, "Comparison not found")
if comp.winner:
raise HTTPException(400, "Already voted")
mapping = json.loads(comp.blind_mapping) if comp.blind_mapping else {"left": "a", "right": "b"}
if winner == "tie":
comp.winner = "tie"
elif winner == "left":
comp.winner = mapping["left"]
elif winner == "right":
comp.winner = mapping["right"]
else:
raise HTTPException(400, "winner must be 'left', 'right', or 'tie'")
comp.voted_at = datetime.utcnow()
db.commit()
return {
"winner": comp.winner,
"model_a": comp.model_a,
"model_b": comp.model_b,
"revealed": {
"left": comp.model_a if mapping["left"] == "a" else comp.model_b,
"right": comp.model_a if mapping["right"] == "a" else comp.model_b,
},
}
finally:
db.close()
@router.post("/record")
def record_comparison(request: Request, body: RecordVoteRequest):View on GitHub (pinned to f9235ebbf1)
Solutions
- Send exactly one of the lowercase strings 'left', 'right', 'tie'.
- Pin the allowed set client-side and derive it from one constant shared with the API docs.
- Add a client-side guard before POSTing.
Example fix
// before
formData.set('winner', side.toUpperCase()) // 'LEFT' -> 400
// after
const WINNERS = new Set(['left','right','tie'])
if (!WINNERS.has(side)) throw new Error('bad winner')
formData.set('winner', side) Defensive patterns
Strategy: type-guard
Validate before calling
WINNERS = {'left', 'right', 'tie'}
assert winner in WINNERS, f'winner must be one of {WINNERS}' Type guard
const isWinner = (w: string): w is 'left'|'right'|'tie' => w === 'left' || w === 'right' || w === 'tie'
Prevention
- Keep one shared constant for the winner enum across client and docs.
- Unit-test the vote path with uppercase/bad values to catch enum drift.
When it happens
Trigger: POST /{comp_id}/vote with winner=form value 'A', 'model_a', 'Left', empty string, or any UI enum that drifted from the API contract.
Common situations: Frontend label refactor changing the submitted value; older client using the pre-blind 'a'/'b' convention; form default left blank.
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/6c0dc363686a65f3.
Report an issue: GitHub.