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

  1. Use the uid string exactly as the server returned it in GET /events for that occurrence — do not synthesize it client-side.
  2. Refresh the event list and retry if the event's recurrence rule was modified since the list was loaded.
  3. 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

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


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/fe04be0dd68cd2f5. Report an issue: GitHub.