Skip to content

fix: surface actionable delete-failure messages instead of a bare HTTP 500#352

Merged
lstein merged 2 commits into
masterfrom
lstein/fix/informative-delete-errors
Jul 16, 2026
Merged

fix: surface actionable delete-failure messages instead of a bare HTTP 500#352
lstein merged 2 commits into
masterfrom
lstein/fix/informative-delete-errors

Conversation

@lstein

@lstein lstein commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

Deleting an image the server can't remove (e.g. a read-only folder) popped a dialog reading:

Failed to delete: HTTP 500 Internal Server Error for delete_image/family/4425?move_to_trash=true

Two failures stacked: the backend wrapped the PermissionError in a generic 500, and the frontend showed HttpError.message while the server's explanation sat unread in error.body.detail. This fixes both halves.

Changes

Backend (routers/index.py)

  • New _remove_image_file() helper wraps the trash/unlink step of delete_image and translates OS failures into HTTP errors whose detail tells the user what to fix:
    • PermissionError403 — "Cannot delete 'img.jpg': permission denied. The PhotoMap server needs write permission on the image file and its containing folder."
    • send2trash's TrashPermissionError403 — the file may be deletable even though the trash folder isn't usable, so the message suggests switching "When deleting" to "Just Delete" in Settings.
    • Other OSError500, but the detail now carries the OS reason (e.g. "Invalid cross-device link" from the known no-trash-dir-on-mount case) plus the same Settings hint when trashing was what failed.

Frontend

  • New errorDetail(error) helper in utils.js prefers the server's {detail: "..."} string over the generic HTTP <status> for <url> message (string details only — FastAPI validation lists fall back to the generic message).
  • The delete alerts in control-panel.js and bookmarks.js use it.

Testing

  • 3 new backend tests: unlink PermissionError → 403 naming the file (and confirming the failed delete leaves file + index row intact), TrashPermissionError → 403 with the Settings suggestion, generic trash OSError → 500 carrying the OS reason.
  • 2 new Jest tests for errorDetail (prefers detail; falls back for missing/non-string detail and non-HTTP errors).
  • Full suites green (27 backend in test_index.py, 424 frontend); ruff / eslint / prettier clean.
  • Also exercised against a genuinely read-only directory (chmod 555): both the unlink and trash paths produce the messages above, and manually verified in the running app.

Follow-up

Once this and #351 are both merged, the batch delete_images endpoint's per-item errors list (currently raw exception strings) should reuse _remove_image_file() for the same actionable wording.

🤖 Generated with Claude Code

…P 500

Deleting an image from a read-only folder popped a dialog reading
"Failed to delete: HTTP 500 Internal Server Error for delete_image/..."
— the server knew it was a permissions problem but the reason never
reached the user.

Backend: the file-removal step of delete_image now translates OS
failures into HTTP errors with actionable detail — PermissionError
becomes a 403 naming the file and pointing at write permissions;
send2trash's TrashPermissionError becomes a 403 suggesting the
'Just Delete' setting (the file may be deletable even when the trash
folder isn't usable); other OSErrors keep the 500 but carry the OS
reason and the same Settings hint when trashing was the failure.

Frontend: new errorDetail() helper in utils.js prefers the server's
{detail: ...} explanation over HttpError's generic message; the delete
alerts in control-panel.js and bookmarks.js use it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lstein
lstein enabled auto-merge (rebase) July 16, 2026 20:31
@lstein
lstein merged commit 0a17be3 into master Jul 16, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant