{"record":{"id":"3a8e7b2de4417691","repo":"jamiepine/voicebox","slug":"only-failed-generations-can-be-retried","errorCode":null,"errorMessage":"Only failed generations can be retried","messagePattern":"Only failed generations can be retried","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"backend/routes/generations.py","lineNumber":156,"sourceCode":"            instruct=data.instruct,\n            mode=\"generate\",\n            max_chunk_chars=data.max_chunk_chars,\n            crossfade_ms=data.crossfade_ms,\n        )\n    )\n\n    return generation\n\n\n@router.post(\"/generate/{generation_id}/retry\", response_model=models.GenerationResponse)\nasync def retry_generation(generation_id: str, db: Session = Depends(get_db)):\n    \"\"\"Retry a failed generation using the same parameters.\"\"\"\n    gen = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not gen:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n\n    if (gen.status or \"completed\") != \"failed\":\n        raise HTTPException(status_code=400, detail=\"Only failed generations can be retried\")\n\n    gen.status = \"generating\"\n    gen.error = None\n    gen.audio_path = \"\"\n    gen.duration = 0\n    db.commit()\n    db.refresh(gen)\n\n    task_manager = get_task_manager()\n    task_manager.start_generation(\n        task_id=generation_id,\n        profile_id=gen.profile_id,\n        text=gen.text,\n    )\n\n    enqueue_generation(\n        generation_id,\n        run_generation(","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/generations.py#L138-L174","documentation":"Returned as HTTP 400 by POST /generate/{generation_id}/retry. The guard `(gen.status or 'completed') != 'failed'` only allows retry when the row's status is exactly 'failed'. A NULL status defaults to 'completed' (so it is not retryable), and any other status ('generating', 'completed', etc.) is also rejected. Retry is exclusively for failed generations.","triggerScenarios":"Retrying a generation whose status is 'completed' (already succeeded), 'generating' (still running), or NULL (treated as completed); retrying after a successful retry already flipped status to 'generating'.","commonSituations":"User clicks retry on a generation that has since succeeded; double-click on retry where the first call moved status to 'generating'; a legacy row with NULL status that the UI mislabels as failed; retrying a generation that is mid-flight.","solutions":["Only show the retry action when generation.status === 'failed'.","After a successful retry call, optimistically mark the row 'generating' in the UI and disable retry.","Refresh the generation status before retry to catch rows that recovered or are in flight.","For NULL-status rows, treat them as completed and offer regenerate, not retry."],"exampleFix":"// before\nretryBtn.onclick = () => post(`/generate/${id}/retry`);\n// after\nretryBtn.disabled = generation.status !== 'failed';\nretryBtn.onclick = async () => {\n  await post(`/generate/${id}/retry`);\n  generation.status = 'generating'; // optimistic\n  retryBtn.disabled = true;\n};","handlingStrategy":"validation","validationCode":"status = (gen.status or 'completed')\nif status != 'failed':\n    raise NotRetryable(status)  # would 400\n# safe to POST /generate/{id}/retry","typeGuard":"def is_retryable(gen) -> bool:\n    return (getattr(gen, 'status', None) or 'completed') == 'failed'","tryCatchPattern":"try:\n    client.post(f'/generate/{generation_id}/retry')\nexcept HTTPStatusError as e:\n    if e.response.status_code == 400 and 'failed' in e.response.json()['detail']:\n        refresh_generation_status(generation_id)  # status changed since page load\n        return\n    raise","preventionTips":["Only show retry when status === 'failed'.","After a retry call, optimistically mark status 'generating' and disable retry.","Refresh status before retry to catch rows that recovered or are in flight.","Treat NULL-status rows as completed — offer regenerate, not retry."],"tags":["fastapi","http-400","generate","retry","state-machine","status-guard","rest"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}