{"record":{"id":"fe04be0dd68cd2f5","repo":"odysseus-dev/odysseus","slug":"invalid-recurring-occurrence-uid","errorCode":null,"errorMessage":"Invalid recurring occurrence uid","messagePattern":"Invalid recurring occurrence uid","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/calendar_routes.py","lineNumber":1334,"sourceCode":"        finally:\n            db.close()\n\n    @router.delete(\"/events/{uid}\")\n    async def delete_event(request: Request, uid: str, scope: str = \"series\"):\n        owner = _require_user(request)\n        try:\n            base_uid = _resolve_base_uid(uid)\n        except ValueError as e:\n            raise HTTPException(400, str(e))\n        db = SessionLocal()\n        try:\n            ev = _get_or_404_event(db, base_uid, owner)\n            is_occurrence_delete = scope in {\"occurrence\", \"instance\"} and \"::\" in uid and bool(ev.rrule)\n            is_caldav = ev.calendar and ev.calendar.source == \"caldav\"\n            if is_occurrence_delete:\n                key = _occurrence_exdate_key(uid, ev)\n                if not key:\n                    raise HTTPException(400, \"Invalid recurring occurrence uid\")\n                exdates = _recurrence_exdates(ev)\n                if key not in exdates:\n                    exdates.append(key)\n                ev.recurrence_exdates = json.dumps(sorted(exdates))\n                if is_caldav:\n                    ev.caldav_sync_pending = \"update\"\n                db.commit()\n                if is_caldav:\n                    await _push_caldav_event_after_commit(owner, base_uid, \"update\")\n                return {\"ok\": True, \"scope\": \"occurrence\", \"exdate\": key}\n            if is_caldav:\n                _record_caldav_delete_tombstone(db, ev, owner)\n            db.delete(ev)\n            db.commit()\n            if is_caldav:\n                await _push_caldav_event_after_commit(owner, base_uid, \"delete\")\n            return {\"ok\": True}\n        except HTTPException:","sourceCodeStart":1316,"sourceCodeEnd":1352,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/calendar_routes.py#L1316-L1352","documentation":"Raised as HTTP 400 by DELETE /events/{uid}?scope=occurrence when _occurrence_exdate_key(uid, ev) returns a falsy value. That helper derives the EXDATE key (the date of the occurrence to exclude) from the compound uid and the event's rrule/dtstart; it fails when the '::'-suffixed uid does not carry a parseable occurrence date or the derived key does not line up with the recurrence rule, so the server refuses rather than writing a meaningless EXDATE.","triggerScenarios":"scope=occurrence on a uid whose suffix is not the occurrence datetime (e.g. 'base::red'); suffix datetime that does not correspond to any occurrence of the rrule; malformed date format in the suffix; client generating occurrence uids locally instead of copying them from GET /events.","commonSituations":"Frontend constructing occurrence ids from a client-side date library with a different format (no 'T', milliseconds included); editing across timezones shifting the occurrence key; deleting an occurrence of an event whose rrule changed since the list was fetched, so the old key no longer matches.","solutions":["Use the uid string exactly as the server returned it in GET /events for that occurrence — do not synthesize it client-side.","Refresh the event list and retry if the event's recurrence rule was modified since the list was loaded.","Fall back to scope=series to delete the entire event if per-occurrence deletion keeps failing."],"exampleFix":"// before\nconst occUid = `${baseUid}::${dayjs(occ.start).format('YYYY-MM-DD')}`; // wrong format\nawait fetch(`/events/${occUid}?scope=occurrence`, {method: 'DELETE'});\n\n// after\nawait fetch(`/events/${encodeURIComponent(serverOcc.uid)}?scope=occurrence`, {method: 'DELETE'});","handlingStrategy":"validation","validationCode":"// occurrence delete: uid must come from the server's event list\nconst occ = eventsFromServer.flat().find(e => e.uid === uid);\nif (!occ || !occ.uid.includes('::')) throw new Error('not a server-issued occurrence uid');\nawait fetch(`/events/${encodeURIComponent(occ.uid)}?scope=occurrence`, {method: 'DELETE'});","typeGuard":"function isOccurrenceUid(uid: unknown): uid is string {\n  return typeof uid === 'string' && /^[^:]+::.+/.test(uid);\n}","tryCatchPattern":"try { await api.del(`/events/${uid}`, {params: {scope: 'occurrence'}}); }\ncatch (e) {\n  if (e.status === 400) { // stale occurrence key — refetch and retry once\n    await refetchEvents();\n    return api.del(`/events/${freshUid}`, {params: {scope: 'occurrence'}});\n  }\n  throw e;\n}","preventionTips":["Always copy occurrence uids from the server response; do not build them from local dates.","Refetch events before deleting an occurrence if the series was edited since your list was loaded.","Fall back to scope=series when per-occurrence delete repeatedly fails."],"tags":["http-400","recurrence","rrule","fastapi","exdate"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}