odysseus-dev/odysseus · warning · HTTPException
Invalid recurring occurrence uid
Error message
Invalid recurring occurrence uid
What it means
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.
Source
Thrown at routes/calendar_routes.py:1334
finally:
db.close()
@router.delete("/events/{uid}")
async def delete_event(request: Request, uid: str, scope: str = "series"):
owner = _require_user(request)
try:
base_uid = _resolve_base_uid(uid)
except ValueError as e:
raise HTTPException(400, str(e))
db = SessionLocal()
try:
ev = _get_or_404_event(db, base_uid, owner)
is_occurrence_delete = scope in {"occurrence", "instance"} and "::" in uid and bool(ev.rrule)
is_caldav = ev.calendar and ev.calendar.source == "caldav"
if is_occurrence_delete:
key = _occurrence_exdate_key(uid, ev)
if not key:
raise HTTPException(400, "Invalid recurring occurrence uid")
exdates = _recurrence_exdates(ev)
if key not in exdates:
exdates.append(key)
ev.recurrence_exdates = json.dumps(sorted(exdates))
if is_caldav:
ev.caldav_sync_pending = "update"
db.commit()
if is_caldav:
await _push_caldav_event_after_commit(owner, base_uid, "update")
return {"ok": True, "scope": "occurrence", "exdate": key}
if is_caldav:
_record_caldav_delete_tombstone(db, ev, owner)
db.delete(ev)
db.commit()
if is_caldav:
await _push_caldav_event_after_commit(owner, base_uid, "delete")
return {"ok": True}
except HTTPException:View on GitHub (pinned to f9235ebbf1)
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.
Example fix
// before
const occUid = `${baseUid}::${dayjs(occ.start).format('YYYY-MM-DD')}`; // wrong format
await fetch(`/events/${occUid}?scope=occurrence`, {method: 'DELETE'});
// after
await fetch(`/events/${encodeURIComponent(serverOcc.uid)}?scope=occurrence`, {method: 'DELETE'}); Defensive patterns
Strategy: validation
Validate before calling
// occurrence delete: uid must come from the server's event list
const occ = eventsFromServer.flat().find(e => e.uid === uid);
if (!occ || !occ.uid.includes('::')) throw new Error('not a server-issued occurrence uid');
await fetch(`/events/${encodeURIComponent(occ.uid)}?scope=occurrence`, {method: 'DELETE'}); Type guard
function isOccurrenceUid(uid: unknown): uid is string {
return typeof uid === 'string' && /^[^:]+::.+/.test(uid);
} Try / catch
try { await api.del(`/events/${uid}`, {params: {scope: 'occurrence'}}); }
catch (e) {
if (e.status === 400) { // stale occurrence key — refetch and retry once
await refetchEvents();
return api.del(`/events/${freshUid}`, {params: {scope: 'occurrence'}});
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Failed to list events
- Current password is incorrect
- {key} must be an integer
- Password is required
- Invalid ICS file: {e}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/fe04be0dd68cd2f5.
Report an issue: GitHub.