jamiepine/voicebox · error · HTTPException

Only failed generations can be retried

Error message

Only failed generations can be retried

What it means

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.

Source

Thrown at backend/routes/generations.py:156

            instruct=data.instruct,
            mode="generate",
            max_chunk_chars=data.max_chunk_chars,
            crossfade_ms=data.crossfade_ms,
        )
    )

    return generation


@router.post("/generate/{generation_id}/retry", response_model=models.GenerationResponse)
async def retry_generation(generation_id: str, db: Session = Depends(get_db)):
    """Retry a failed generation using the same parameters."""
    gen = db.query(DBGeneration).filter_by(id=generation_id).first()
    if not gen:
        raise HTTPException(status_code=404, detail="Generation not found")

    if (gen.status or "completed") != "failed":
        raise HTTPException(status_code=400, detail="Only failed generations can be retried")

    gen.status = "generating"
    gen.error = None
    gen.audio_path = ""
    gen.duration = 0
    db.commit()
    db.refresh(gen)

    task_manager = get_task_manager()
    task_manager.start_generation(
        task_id=generation_id,
        profile_id=gen.profile_id,
        text=gen.text,
    )

    enqueue_generation(
        generation_id,
        run_generation(

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Only show the retry action when generation.status === 'failed'.
  2. After a successful retry call, optimistically mark the row 'generating' in the UI and disable retry.
  3. Refresh the generation status before retry to catch rows that recovered or are in flight.
  4. For NULL-status rows, treat them as completed and offer regenerate, not retry.

Example fix

// before
retryBtn.onclick = () => post(`/generate/${id}/retry`);
// after
retryBtn.disabled = generation.status !== 'failed';
retryBtn.onclick = async () => {
  await post(`/generate/${id}/retry`);
  generation.status = 'generating'; // optimistic
  retryBtn.disabled = true;
};
Defensive patterns

Strategy: validation

Validate before calling

status = (gen.status or 'completed')
if status != 'failed':
    raise NotRetryable(status)  # would 400
# safe to POST /generate/{id}/retry

Type guard

def is_retryable(gen) -> bool:
    return (getattr(gen, 'status', None) or 'completed') == 'failed'

Try / catch

try:
    client.post(f'/generate/{generation_id}/retry')
except HTTPStatusError as e:
    if e.response.status_code == 400 and 'failed' in e.response.json()['detail']:
        refresh_generation_status(generation_id)  # status changed since page load
        return
    raise

Prevention

When it happens

Trigger: 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'.

Common situations: 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.

Related errors


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/3a8e7b2de4417691. Report an issue: GitHub.