Recurring event exceptions (RECURRENCE-ID / EXDATE) are silently dropped on CalDAV PUT
-
Calendar: Recurring event exceptions (RECURRENCE-ID / EXDATE) are silently dropped on CalDAV PUT
Summary
When a single occurrence of a recurring event is moved or deleted from a CalDAV client (Apple Calendar on macOS and iOS), the Calendar app accepts the request with a success response but stores only the master event. The exception is lost without any error. Other clients then fetch the event again and fall back to the unmodified series, which also wipes the exception on devices that had shown it before. The published ICS feed never contains any exceptions.
This is silent data loss: users believe their change was saved, but it only exists locally on the editing device until the next resync.
Environment
Cloudron app Calendar ( io.cloudron.calendar) 1.14.2Docker image cloudron/io.cloudron.calendar:202611041546350000Release commit be2a8d35455babde3cc0165113323b5bd1943c78Clients Apple Calendar on macOS and iOS (CalDAV account) Calendar Shared calendar, two users with write access The behaviour was already present in 1.13.0.
Steps to reproduce
- In Apple Calendar (CalDAV account on the Calendar app), create a weekly recurring event, e.g. Wednesdays 08:30–17:30.
- Move a single occurrence to another day and choose "Only this event".
- Check the event on a second device with the same account or a shared user, or open the calendar's published ICS link.
- Optionally, edit the same series on the second device, then look at the first device again.
Expected behaviour
- The moved occurrence is stored on the server as an override (a VEVENT with
RECURRENCE-ID), and a deleted occurrence asEXDATEon the master. - All clients and the ICS feed show the moved or deleted occurrence.
- If the server cannot store exceptions, it should reject the PUT instead of reporting success.
Actual behaviour
- The editing device shows the moved occurrence, but only from its local copy.
- All other devices and the ICS feed show the unmodified series.
- As soon as the event is saved again from another device, the server issues a new ETag. The first device refetches the event and its exception disappears as well.
- In our shared calendar the ICS feed contains 53 recurring series and zero
RECURRENCE-ID,EXDATE,RDATEorSTATUS:CANCELLEDentries, although several occurrences were moved on the clients.
Root cause (from the source in the image)
-
Only the first VEVENT is parsed.
src/ical.js,parseIcsToEvent()(around lines 236–287) is documented as "first VEVENT only" and stops at the firstEND:VEVENT. Apple Calendar sends the master VEVENT plus one VEVENT per modified occurrence (sameUID, withRECURRENCE-ID) in a single resource (RFC 4791 §4.1). Every VEVENT after the first one is ignored. -
The PUT overwrites the stored event and reports success.
src/events.js,saveEventFromIcs()(around lines 556–597) updates the existing row with the parsed first VEVENT and generates a new ETag.src/routes/caldav.js,handlePut()(around lines 359–428) answers204 No Contentwith that ETag. The client therefore considers the change saved. -
No storage for exceptions. The
eventstable (src/database.js, around lines 90–108) has onlyrecurrence_rule. There is no column or table for overrides or excluded dates. -
EXDATE / RECURRENCE-ID / RDATE are not handled anywhere. A search over
src/(excluding tests) finds no occurrence ofEXDATE,RECURRENCE-IDorRDATE. Deleting a single occurrence (which Apple sends asEXDATEon the master) is lost in the same way.
Possible follow-up issue, not observed in practice: because only the first VEVENT is read, a client that puts an override VEVENT before the master would overwrite the series itself with the single occurrence. Apple Calendar happens to send the master first.
Impact
- Moved or deleted occurrences of recurring events are lost for every other user and device, and eventually for the editing device too.
- In shared calendars, one user's edit silently reverts the other users' changes.
- Anything consuming the ICS feed (dashboards, subscriptions) shows wrong dates.
Suggested fix
- Parse all VEVENTs of a resource and store overrides (
RECURRENCE-ID) andEXDATE(ideally alsoRDATE) together with the master event. - Apply them during recurrence expansion and include them in CalDAV GET/REPORT responses and in the ICS export.
- Until that is implemented: reject a PUT that contains
RECURRENCE-IDoverrides orEXDATE(e.g.403with aCALDAV:valid-calendar-object-resourceprecondition, or415) instead of returning204. Clients would then show a sync error instead of silently losing data.
Workaround
Do not move or delete single occurrences of a series. End the series and create a new one, or use individual events instead.
Disclosure: I found this problem and made the observations described above myself, on my own devices and with our shared calendar. The source code analysis and the wording of this report were done with the help of an AI assistant.
-
-
-
N nebulon marked this topic as a question
-
N nebulon has marked this topic as solved
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login