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
- Leave at least one version per generation; if you want it gone, delete the whole generation instead.
- Disable the delete button in the UI when the versions count for that generation is 1.
- To replace the audio, create a new version first, then delete the old one.
- 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
- Disable the delete control when the generation has only one version.
- To replace audio, create the new version first, then delete the old one.
- For full removal, delete the generation (which cascades version cleanup).
- Never iterate-delete down to zero versions; the business rule enforces a floor of one.
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
- Source version not found
- Version not found
- {exception message from delete_preset (ValueError)}
- {e}
- Generation not found
AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12).
Data as JSON: /api/errors/c5b232639d732166.
Report an issue: GitHub.