{"record":{"id":"528e4dddc856bf10","repo":"jamiepine/voicebox","slug":"exception-message-from-delete-preset-valueerror","errorCode":null,"errorMessage":"{exception message from delete_preset (ValueError)}","messagePattern":"\\{exception message from delete_preset \\(ValueError\\)\\}","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"backend/routes/effects.py","lineNumber":135,"sourceCode":"        result = effects_mod.update_preset(preset_id, data, db)\n        if not result:\n            raise HTTPException(status_code=404, detail=\"Preset not found\")\n        return result\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.delete(\"/effects/presets/{preset_id}\")\nasync def delete_effect_preset(preset_id: str, db: Session = Depends(get_db)):\n    \"\"\"Delete a user effect preset.\"\"\"\n    from ..services import effects as effects_mod\n\n    try:\n        if not effects_mod.delete_preset(preset_id, db):\n            raise HTTPException(status_code=404, detail=\"Preset not found\")\n        return {\"status\": \"deleted\"}\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.get(\n    \"/generations/{generation_id}/versions\",\n    response_model=list[models.GenerationVersionResponse],\n)\nasync def list_generation_versions(\n    generation_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"List all versions for a generation.\"\"\"\n    gen = await history.get_generation(generation_id, db)\n    if not gen:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n\n    from ..services import versions as versions_mod\n\n    return versions_mod.list_versions(generation_id, db)","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/effects.py#L117-L153","documentation":"Returned as HTTP 400 by DELETE /effects/presets/{preset_id}. effects.delete_preset() raises ValueError(\"Cannot delete built-in presets\") when the looked-up row has is_builtin=True. The route catches ValueError and surfaces str(e) as the detail. Built-in presets are seed data shipped with the app and are intentionally immutable.","triggerScenarios":"DELETE /effects/presets/{preset_id} where preset_id resolves to a row with is_builtin=True; iterating over GET /effects/presets and calling delete on every entry including built-ins; UI failing to hide/disable the delete button for built-in presets.","commonSituations":"A 'reset to defaults' feature that tries to wipe all presets; bulk-delete scripts that do not filter on is_builtin; frontend that does not read the is_builtin flag on EffectPresetResponse; running a destructive migration without guarding built-ins.","solutions":["Filter the preset list by is_builtin === false before offering delete in the UI.","If bulk cleanup is needed, only delete user presets and treat built-ins as read-only reference data.","Add a client-side guard: if (preset.is_builtin) return; before calling delete.","Re-seed built-ins via the app's seeding path rather than deleting and recreating them."],"exampleFix":"// before\nfor (const p of allPresets) await api.delete(`/effects/presets/${p.id}`);\n// after\nfor (const p of allPresets) {\n  if (p.is_builtin) continue; // built-ins are immutable\n  await api.delete(`/effects/presets/${p.id}`);\n}","handlingStrategy":"validation","validationCode":"# Never attempt to delete built-in presets.\npreset = next((p for p in effects_mod.list_presets(db) if p.id == preset_id), None)\nif preset is None:\n    return  # will 404 instead\nif preset.is_builtin:\n    return  # would 400 — skip; built-ins are immutable\n# safe to delete","typeGuard":"def is_user_preset(preset) -> bool:\n    \"\"\"True only for a deletable, user-created preset.\"\"\"\n    return preset is not None and not bool(preset.is_builtin)","tryCatchPattern":"try:\n    client.delete(f'/effects/presets/{preset_id}')\nexcept HTTPStatusError as e:\n    if e.response.status_code == 400 and 'built-in' in e.response.json()['detail']:\n        # expected when hitting a built-in — filter these out upstream\n        return\n    raise","preventionTips":["Filter the preset list by is_builtin === false before any bulk delete.","Treat built-in presets as read-only reference data in the UI.","Add a client-side guard that disables delete for built-ins.","Re-seed built-ins through the app's seeding path, never via delete+recreate."],"tags":["fastapi","http-400","effects","presets","built-in","valueerror","rest"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}