{"record":{"id":"a92bfbfd6fc6ee1b","repo":"jamiepine/voicebox","slug":"profile-not-found-or-no-avatar-to-delete","errorCode":null,"errorMessage":"Profile not found or no avatar to delete","messagePattern":"Profile not found or no avatar to delete","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"warning","filePath":"backend/routes/profiles.py","lineNumber":277,"sourceCode":"    if not profile.avatar_path:\n        raise HTTPException(status_code=404, detail=\"No avatar found for this profile\")\n\n    avatar_path = config.resolve_storage_path(profile.avatar_path)\n    if avatar_path is None or not avatar_path.exists():\n        raise HTTPException(status_code=404, detail=\"Avatar file not found\")\n\n    return FileResponse(avatar_path)\n\n\n@router.delete(\"/profiles/{profile_id}/avatar\")\nasync def delete_profile_avatar(\n    profile_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Delete avatar image for a profile.\"\"\"\n    success = await profiles.delete_avatar(profile_id, db)\n    if not success:\n        raise HTTPException(status_code=404, detail=\"Profile not found or no avatar to delete\")\n    return {\"message\": \"Avatar deleted successfully\"}\n\n\n@router.get(\"/profiles/{profile_id}/export\")\nasync def export_profile(\n    profile_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Export a voice profile as a ZIP archive.\"\"\"\n    try:\n        profile = await profiles.get_profile(profile_id, db)\n        if not profile:\n            raise HTTPException(status_code=404, detail=\"Profile not found\")\n\n        zip_bytes = export_import.export_profile_to_zip(profile_id, db)\n\n        safe_name = \"\".join(c for c in profile.name if c.isalnum() or c in (\" \", \"-\", \"_\")).strip()\n        if not safe_name:","sourceCodeStart":259,"sourceCodeEnd":295,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/profiles.py#L259-L295","documentation":"DELETE /profiles/{profile_id}/avatar calls profiles.delete_avatar; if it returns falsy the route raises 404 'Profile not found or no avatar to delete'. The service collapses two distinct conditions into one False return -- the profile_id does not exist, or the profile exists but has no avatar_path to clear. Callers cannot tell which from the response alone.","triggerScenarios":"Calling delete on a profile that never had an avatar uploaded; calling delete on a profile_id that was already removed; double-deleting after a successful first delete (the second sees avatar_path already None).","commonSituations":"Frontend always shows a 'remove avatar' button regardless of whether one is set; user clicks remove twice; sync script re-runs.","solutions":["Treat the 404 as success for idempotent avatar deletion (end state: no avatar).","If you need to distinguish the two cases, GET /profiles/{profile_id} first and inspect avatar_path.","Guard the UI delete button so it only appears when an avatar is known to exist."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"async def avatar_exists(client, profile_id) -> bool:\n    profile = (await client.get(f\"/profiles/{profile_id}\")).json()\n    return bool(profile.get(\"avatar_path\"))","typeGuard":null,"tryCatchPattern":"resp = await client.delete(f\"/profiles/{profile_id}/avatar\")\nif resp.status_code == 404:\n    # Either profile is gone or there was no avatar -- both are acceptable end states\n    pass\nelse:\n    resp.raise_for_status()\nrefresh_profile(profile_id)","preventionTips":["Treat this 404 as success for idempotent avatar removal.","Only show the 'remove avatar' control when avatar_path is set.","Refresh the profile after delete to update the UI."],"tags":["fastapi","avatar","delete","not-found","idempotency"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}