From 5eb279ff31b30235839ea064ebd3506dd207067b Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sat, 1 Aug 2026 16:20:45 +0100 Subject: [PATCH] fix(entries): 404 on PUT when the row no longer exists The UPDATE matched nothing but the handler still answered 200 Updated, so the editor reported Saved for an edit that went nowhere - the entry had been deleted by a sync or a purge after the editor opened it. Silent data loss; the client already surfaces a failed save as an error toast. --- TEST_SCENARIOS.md | 13 ++++++++----- delphi-backend/Handlers/PM.Handler.Entries.pas | 11 +++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/TEST_SCENARIOS.md b/TEST_SCENARIOS.md index 859ae31..93dbd74 100644 --- a/TEST_SCENARIOS.md +++ b/TEST_SCENARIOS.md @@ -80,13 +80,16 @@ Préparer : 3 entrées `A`, `B`, `C`, dont `A` dans le dossier `Work` avec le ta Aucun de ces enchaînements ne doit afficher « Entry changed elsewhere » : - **A12a** : `A` ouverte + dirty → Save → modifier encore → Save (le rechargement post-save re-cale la référence)✅ - **A12b** : `A` ouverte + dirty → batch *Add tag* / *Move to folder* sur `A` → Save (l'éditeur a absorbé le changement)✅ -- **A12c** : `A` ouverte + dirty → supprimer son dossier → Save (cf. A7) -- **A12d** : `A` ouverte + dirty → *Fetch missing* favicons → Save (l'icône est hors payload éditeur) -- **A12e** : `A` ouverte + dirty → sync **sans** changement distant sur `A` → Save +- **A12c** : `A` ouverte + dirty → supprimer son dossier → Save (cf. A7)✅ +- **A12d** : `A` ouverte + dirty → *Fetch missing* favicons → Save (l'icône est hors payload éditeur)✅ +- **A12e** : `A` ouverte + dirty → sync **sans** changement distant sur `A` → Save✅ -### A13 · entrée supprimée ailleurs pendant l'édition +### A13 · entrée supprimée ailleurs pendant l'édition ✅ - **ACTION** : `A` ouverte + dirty → un sync distant supprime `A` (tombstone) → Save -- **ATTENDU** : pas de plantage. L'entrée n'est plus en mémoire → le contrôle A11 passe la main ; le PUT échoue proprement avec un toast d'erreur. +- **ATTENDU** : pas de plantage. L'entrée n'est plus en mémoire → le contrôle A11 passe la main ; le PUT échoue proprement avec un **toast d'erreur** — surtout pas « Saved ». +- **Recette locale, sans second appareil** : ouvrir `A` et la rendre dirty → dans DB Browser, app lancée, `DELETE FROM vault_entries WHERE id = ` puis **Write Changes** (sans ça le WAL ne publie rien) → dans l'app, déclencher un `loadEntries()` qui ne concerne pas `A` (mettre une **autre** entrée à la corbeille puis *Empty trash*) → Save. +- **Bug trouvé par ce test** : le serveur répondait **200 « Updated »** pour un `UPDATE` n'ayant touché **aucune** ligne — le client affichait « Saved » alors que rien n'était écrit. Perte de données silencieuse. `HandleUpdateEntry` teste maintenant `RowsAffected` et renvoie 404. +- **Même classe, non corrigé** : delete / restore / favorite / pin / set-icon répondent aussi OK sur un id inexistant. Assumé — aucun ne prétend avoir enregistré un travail de l'utilisateur, et le client recharge la liste juste après. --- diff --git a/delphi-backend/Handlers/PM.Handler.Entries.pas b/delphi-backend/Handlers/PM.Handler.Entries.pas index eb97652..c55c8f0 100644 --- a/delphi-backend/Handlers/PM.Handler.Entries.pas +++ b/delphi-backend/Handlers/PM.Handler.Entries.pas @@ -514,6 +514,7 @@ var LSiteEnc, LSiteIv, LTitleEnc, LTitleIv, LTagsEnc, LTagsIv: string; LHasTemplate: Boolean; LQ: TFDQuery; + LRows: Integer; begin try LUserId := Authenticate(ARequest, AResponse); @@ -664,6 +665,7 @@ begin LQ.ParamByName('id').AsInteger := LId; LQ.ParamByName('uid').AsInteger := LUserId; LQ.ExecSQL; + LRows := LQ.RowsAffected; finally LQ.Free; end; @@ -671,6 +673,15 @@ begin DB.Unlock; end; + // No row matched: the entry was deleted (another device's sync, a purge) + // between the editor opening it and this save. Reporting 200 here made the + // client show "Saved" for an edit that went nowhere — silent data loss. + if LRows <= 0 then + begin + TJSONHelper.SendError(AResponse, 404, 'Entry no longer exists'); + Exit; + end; + LogAudit(LUserId, 'edit_entry', GetClientIP(ARequest)); TJSONHelper.SendOK(AResponse, 'Updated'); end;