jamiepine/voicebox · warning · HTTPException
Only active generations can be cancelled
Error message
Only active generations can be cancelled
What it means
Returned by POST /generate/{generation_id}/cancel when the generation row exists but its status is not in the active set ("loading_model", "generating"). The guard reads `(gen.status or "completed")`, so a NULL/empty status is treated as "completed" and is therefore non-cancellable. HTTP 400. The endpoint refuses to send a cancel signal to a job that is already terminal or was never started.
Source
Thrown at backend/routes/generations.py:243
seed=gen.seed,
instruct=gen.instruct,
mode="regenerate",
version_id=version_id,
)
)
return models.GenerationResponse.model_validate(gen)
@router.post("/generate/{generation_id}/cancel")
async def cancel_generation(generation_id: str, db: Session = Depends(get_db)):
"""Cancel a queued or running generation."""
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") not in ("loading_model", "generating"):
raise HTTPException(status_code=400, detail="Only active generations can be cancelled")
cancellation_state = cancel_generation_job(generation_id)
if cancellation_state is None:
# Row says active but the worker is no longer tracking it — the gen
# coroutine exited without writing a terminal status (most often a
# SQLite lock racing with the failed-status write inside the worker's
# exception handler). Fail the row here so the user can move on.
task_manager = get_task_manager()
task_manager.complete_generation(generation_id)
await history.update_generation_status(
generation_id=generation_id,
status="failed",
db=db,
error="Generation orphaned by worker",
)
return {"message": "Orphaned generation cleared"}
if cancellation_state == "queued":View on GitHub (pinned to 51f49dea19)
Solutions
- Disable the cancel button in the UI as soon as the status stream reports completed/failed — do not wait for user reaction.
- If the row is stuck "queued" but not cancellable, manually update the status to "failed" in the DB or call the orphan-clear path rather than relying on cancel.
- Treat 400 from this endpoint as informational (job already terminal) and refresh the row's status from GET /history/{id}.
- For legacy NULL-status rows, run a one-time migration setting status to a terminal value.
Example fix
// before
<button onClick={() => cancel(id)}>Cancel</button>
// after: gate on known active status
<button disabled={!['loading_model','generating'].includes(row.status)}
onClick={() => cancel(id)}>Cancel</button> Defensive patterns
Strategy: validation
Validate before calling
// Only call cancel when the row is in an active state
const ACTIVE = new Set(['loading_model', 'generating']);
if (!ACTIVE.has(row.status)) { /* don't call cancel */ return; }
await fetch(`/generate/${id}/cancel`, { method:'POST' }); Type guard
const ACTIVE_STATES = new Set(['loading_model', 'generating']);
function isActiveGeneration(gen) {
return gen != null && ACTIVE_STATES.has(gen.status);
} Try / catch
try {
const res = await fetch(`/generate/${id}/cancel`, { method: 'POST' });
if (res.status === 400) {
// already terminal (or queued not cancellable) — refresh status
await refreshStatus(id);
return;
}
} catch (e) { console.error(e); } Prevention
- Gate the cancel button on the live SSE status, not on user perception.
- Treat NULL status rows as completed (the server does).
- Note that 'queued' status is NOT cancellable via this endpoint.
When it happens
Trigger: Cancelling a generation that already finished (status "completed"), already failed (status "failed"), or whose status column is NULL (treated as completed). Also triggered if a generation is still "queued" but queued is not in the active tuple — note only loading_model/generating are accepted.
Common situations: User double-clicks cancel after the job already completed; the SSE status stream hasn't updated the UI yet so the user clicks cancel on a now-terminal job; legacy rows with NULL status created before the status field was added; cancelling a queued job (status=="queued") which the guard does NOT permit.
Related errors
- No profile selected
- Model ${model_size} is not downloaded yet. Use /generate to
- Model {model_size} is not downloaded yet. Use /generate to t
- {display} model is not downloaded yet. Use /generate to trig
- Unknown TTS engine: {engine}. Supported: {list(TTS_ENGINES.k
AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12).
Data as JSON: /api/errors/6be9831c97dcfa4b.
Report an issue: GitHub.