jamiepine/voicebox · error · HTTPException

Cannot delete the last remaining version

Error message

Cannot delete the last remaining version

What it means

Returned as HTTP 400 by DELETE /generations/{generation_id}/versions/{version_id}. The route calls versions.delete_version(), which counts the generation's versions and returns False when count <= 1. The last remaining version cannot be deleted because every generation must keep at least one playable audio file (the default/clean version).

Source

Thrown at backend/routes/effects.py:258

        raise HTTPException(status_code=404, detail="Version not found")
    return result


@router.delete("/generations/{generation_id}/versions/{version_id}")
async def delete_generation_version(
    generation_id: str,
    version_id: str,
    db: Session = Depends(get_db),
):
    """Delete a version. Cannot delete the last remaining version."""
    from ..services import versions as versions_mod

    version = versions_mod.get_version(version_id, db)
    if not version or version.generation_id != generation_id:
        raise HTTPException(status_code=404, detail="Version not found")

    if not versions_mod.delete_version(version_id, db):
        raise HTTPException(
            status_code=400,
            detail="Cannot delete the last remaining version",
        )
    return {"status": "deleted"}

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Leave at least one version per generation; if you want it gone, delete the whole generation instead.
  2. Disable the delete button in the UI when the versions count for that generation is 1.
  3. To replace the audio, create a new version first, then delete the old one.
  4. If the goal is full removal, call DELETE /generations/{id} which cascades version cleanup.

Example fix

// before
for (const v of versions) await api.delete(`/generations/${genId}/versions/${v.id}`);
// after: never strip the last version — delete the generation instead
if (versions.length <= 1) {
  await api.delete(`/generations/${genId}`);
} else {
  for (const v of versions.slice(0, -1)) await api.delete(`/generations/${genId}/versions/${v.id}`);
}
Defensive patterns

Strategy: validation

Validate before calling

count = (db.query(DBGenerationVersion).filter_by(generation_id=generation_id).count())
if count <= 1:
    raise LastVersionProtected(generation_id)  # would 400
# safe to DELETE /generations/{gen_id}/versions/{ver_id}

Type guard

def has_more_than_one_version(versions: list) -> bool:
    return len(versions) > 1

Try / catch

try:
    client.delete(f'/generations/{gen_id}/versions/{ver_id}')
except HTTPStatusError as e:
    if e.response.status_code == 400 and 'last remaining' in e.response.json()['detail']:
        # to remove all audio, delete the whole generation instead
        client.delete(f'/generations/{gen_id}')
        return
    raise

Prevention

When it happens

Trigger: Deleting the only version attached to a generation; deleting the second-to-last when the count query sees only one (race with another delete); deleting the last user-created version when no other versions exist.

Common situations: User trying to clear all versions of a generation through the UI; an automated cleanup script that iterates versions without leaving one; a generation that never had a clean version created and only has the derived one.

Related errors


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