From 93ec7cbb52f5bdbfd01423a5a8c0bd952f50b4c1 Mon Sep 17 00:00:00 2001 From: holden093 Date: Mon, 22 Jun 2026 18:39:44 +0200 Subject: [PATCH] fix(contacts): verify UID removal after CardDAV DELETE (#4642) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a post-delete verification step: after the CardDAV server returns 2xx/404, force-re-fetch the contact list and confirm the UID is gone. If the UID is still present, log a warning and return False instead of silently reporting success. This catches the case where _resolve_resource_url falls back to the guessed {uid}.vcf URL but the contact's real resource URL differs — the DELETE hits the wrong URL, server returns 404 (treated as success), but the contact remains. Previously this caused silent persistence failures and agent loops. --- routes/contacts_routes.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/routes/contacts_routes.py b/routes/contacts_routes.py index 2b357216a..de0a5d041 100644 --- a/routes/contacts_routes.py +++ b/routes/contacts_routes.py @@ -689,15 +689,24 @@ def _delete_contact(uid: str) -> bool: url = _resolve_resource_url(uid) auth = (cfg["username"], cfg["password"]) if cfg["username"] else None r = httpx.delete(url, auth=auth, timeout=10) - if r.status_code in (200, 204): - _contact_cache["fetched_at"] = None - return True - if r.status_code == 404: - # Resource not found at the resolved URL. With href resolution - # this should be rare (genuinely already deleted). Invalidate - # the cache and report success so the UI doesn't keep a ghost. - logger.info(f"CardDAV DELETE 404 for {uid} — treating as already gone") + if r.status_code in (200, 204, 404): + # Invalidate cache so the next fetch sees the server truth. _contact_cache["fetched_at"] = None + # Verify: force a fresh fetch and check the UID is actually gone. + # A 404 on the guessed URL ({uid}.vcf) can mean the contact + # lives at a different resource URL — the DELETE missed it but + # we'd silently report success. This check catches that. + fresh = _fetch_contacts(force=True) + still_there = any(c.get("uid") == uid for c in fresh) + if still_there: + logger.warning( + f"CardDAV DELETE reported success for {uid} " + f"but UID still present after re-fetch — " + f"resource URL may differ from {url}" + ) + return False + if r.status_code == 404: + logger.info(f"CardDAV DELETE 404 for {uid} — already gone") return True logger.warning(f"CardDAV DELETE returned {r.status_code}: {r.text[:200]}") return False