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

  1. Send exactly one of the lowercase strings 'left', 'right', 'tie'.
  2. Pin the allowed set client-side and derive it from one constant shared with the API docs.
  3. 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

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.