odysseus-dev/odysseus · info · HTTPException
Already voted
Error message
Already voted
What it means
Raised by the vote endpoint when comp.winner is already set — each comparison accepts exactly one vote. Voting is terminal state; the reveal (model names) has already happened for that comparison.
Source
Thrown at routes/compare/compare_routes.py:255
@router.post("/{comp_id}/vote")
def vote_comparison(
request: Request,
comp_id: str,
winner: str = Form(...), # "left", "right", or "tie"
):
"""Record the user's vote and reveal model names if blind."""
user = get_current_user(request)
db = SessionLocal()
try:
comp = db.query(Comparison).filter(Comparison.id == comp_id).first()
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,View on GitHub (pinned to f9235ebbf1)
Solutions
- Treat 400 'Already voted' as success-equivalent in the client: fetch the comparison to read the revealed winner.
- Disable the vote buttons after the first successful response or once comp.winner is shown.
- Make the vote button idempotent client-side (lock during in-flight request).
Example fix
// before
if (err.status === 400) throw err // double vote crashes UI
// after
try { await vote(id, w) } catch (e) {
if (e.status === 400 && /Already voted/.test(e.message)) { /* refetch reveal */ }
else throw e
} Defensive patterns
Strategy: fallback
Validate before calling
comp = get_comparison(comp_id)
if comp.get('winner'): return comp # already voted — read reveal, skip POST Type guard
const alreadyVoted = (c: {winner?: string|null}) => !!c.winner Try / catch
try: vote(comp_id, side)
except HTTPException as e:
if e.status_code == 400 and 'Already voted' in str(e.detail):
return get_comparison(comp_id) # converge on revealed state
raise Prevention
- Disable vote controls the moment a winner is displayed.
- Lock the vote button while the POST is in flight (double-click guard).
- On network-error retry, first GET the comparison to check winner.
When it happens
Trigger: POST /{comp_id}/vote a second time, from the same or another tab/session, after a successful vote recorded comp.winner and voted_at.
Common situations: Double-click on the vote button firing two POSTs; page reload resubmitting the form; retrying after a network error where the first vote actually landed.
Related errors
- winner must be 'left', 'right', or 'tie'
- await res.text()
- HTTP ${res.status}
- data.error || ('HTTP ' + (data.status || 500))
- model returned no rewritten text
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/0c86b87781731987.
Report an issue: GitHub.