From ba5f2f644221855c624b9a3bfaa950ab1c5ba676 Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Wed, 11 Nov 2026 16:07:06 +0100 Subject: [PATCH 1/2] fix: Bedework refuses a taken name with 500 Bedework (3.10 and 5.0) answers a PUT of a new object under a name another object holds with 500 "duplicatename", whether or not it carries If-None-Match: *; only the same UID gets the 412. With save.if-none-match "ungraceful", save(no_overwrite=True) checks the name with a GET instead. Bedework 3.10 also drops an X- parameter on SUMMARY. Both found with caldav-server-tester, the first one with a probe that now also PUTs another UID. Prompt: [pick from the task list: "calendaring: remaining conformance failures on Ox, Zimbra and others"] Followup-Prompt: ... First priority is to get the conformance tests passing for all the caldav servers available ... Assisted-By: Claude Opus 5.5 Reviewed-by: Tobias Brox --- caldav/compatibility_hints.py | 12 +++++++++++- changes/+bedework-hints.fixed.md | 1 + ...todo-values-x-parameter-multiget-deleted.added.md | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 changes/+bedework-hints.fixed.md diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index b5b01ca4..6ec7d59c 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -965,7 +965,7 @@ class FeatureSet: "links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-5.3.4"], }, "save.if-none-match": { - "description": "A PUT carrying If-None-Match: * to an existing object is refused with 412 Precondition Failed, and creates a missing object. RFC 9110 section 13.1.2 has '*' hold only when the resource has no current representation, so it means 'create, but never overwrite'; RFC 4791 section 5.3.2 tells CalDAV clients to send it when creating. save(no_overwrite=True) sends it, so the server enforces 'do not overwrite' in the same request rather than after a search. When 'unsupported', the server overwrites the existing object (SOGo ignores the header; Zimbra compares '*' as a literal etag, so the condition always holds) - the library then does not send it, nor anywhere else this is not supported ('full' or 'quirk'), and relies on the UID search before the PUT, as before. The default is 'full': RFC 4791 section 2 requires a CalDAV server to support ETags as RFC 2616 defines them, and RFC 2616 section 14.26 (now RFC 9110 section 13.1.2) says the server MUST NOT perform the method when '*' is given and the object exists.", + "description": "A PUT carrying If-None-Match: * to an existing object is refused with 412 Precondition Failed, and creates a missing object. RFC 9110 section 13.1.2 has '*' hold only when the resource has no current representation, so it means 'create, but never overwrite'; RFC 4791 section 5.3.2 tells CalDAV clients to send it when creating. save(no_overwrite=True) sends it, so the server enforces 'do not overwrite' in the same request rather than after a search. When 'unsupported', the server overwrites the existing object (SOGo ignores the header; Zimbra compares '*' as a literal etag, so the condition always holds) - the library then does not send it, nor anywhere else this is not supported ('full' or 'quirk'), and relies on the UID search before the PUT, as before. The default is 'full': RFC 4791 section 2 requires a CalDAV server to support ETags as RFC 2616 defines them, and RFC 2616 section 14.26 (now RFC 9110 section 13.1.2) says the server MUST NOT perform the method when '*' is given and the object exists. 'ungraceful' when the existing object is kept, but refused otherwise than with 412 (Bedework: 500 'duplicatename' for a new object under a name another object holds): the library cannot tell that from another error, so it treats it as unsupported and checks with a GET or a search.", "default": {"support": "full"}, "links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-5.3.2", "https://datatracker.ietf.org/doc/html/rfc4791#section-2", "https://datatracker.ietf.org/doc/html/rfc2616#section-14.26", "https://datatracker.ietf.org/doc/html/rfc9110#section-13.1.2"], }, @@ -2025,10 +2025,16 @@ def __getattr__(name: str) -> Any: "create-calendar.with-supported-component-types.multiple": {"support": "broken", "behaviour": "asked for VEVENT and VTODO, the calendar reports one concatenated name, ['VEVENTVTODO']"}, ## Probed 2026-10-26 by caldav-server-tester "save-load.icalendar.participant": {"support": "ungraceful", "behaviour": "an event with a PARTICIPANT is refused (403)"}, + ## Probed 2026-11-11 by caldav-server-tester + "save-load.icalendar.x-parameter": {"support": "unsupported", "behaviour": "X-CALDAV-SERVER-TESTER on SUMMARY is dropped, in an event"}, ## Measured with caldav-server-tester, 2026-10-28. "save.no-uid-conflict": {"support": "full"}, "save.no-uid-conflict.href-given": {"support": "unsupported"}, "save.no-uid-conflict.if-none-match": {"support": "quirk", "status": 412, "precondition": False}, + ## Probed 2026-11-11 by caldav-server-tester: a new object under a taken + ## name, the case save(no_overwrite=True) guards, is refused with 500 + ## "duplicatename", with or without the header; the same UID gets 412. + "save.if-none-match": {"support": "ungraceful", "behaviour": "If-None-Match: * on an existing object answered 500 rather than 412, for another UID"}, ## If tests are yielding unexpected results, try to increase this: 'search-cache': {'behaviour': 'delay', 'delay': 3}, 'scheduling.calendar-user-address-set': {'support': 'full'}, @@ -2108,6 +2114,10 @@ def __getattr__(name: str) -> Any: "save.no-uid-conflict": {"support": "full"}, "save.no-uid-conflict.href-given": {"support": "unsupported"}, "save.no-uid-conflict.if-none-match": {"support": "quirk", "status": 412, "precondition": False}, + ## Probed 2026-11-11 by caldav-server-tester: a new object under a taken + ## name, the case save(no_overwrite=True) guards, is refused with 500 + ## "duplicatename", with or without the header; the same UID gets 412. + "save.if-none-match": {"support": "ungraceful", "behaviour": "If-None-Match: * on an existing object answered 500 rather than 412, for another UID"}, ## Writes are asynchronous: a read-back issued immediately after a PUT may ## 404 or hand back the pre-write copy. However, in some cases the ## delay is small and varies, sometimes the caldav-server-tester tool will diff --git a/changes/+bedework-hints.fixed.md b/changes/+bedework-hints.fixed.md new file mode 100644 index 00000000..69080858 --- /dev/null +++ b/changes/+bedework-hints.fixed.md @@ -0,0 +1 @@ +On Bedework (3.10 and 5.0), `save(no_overwrite=True)` for a name another object already holds raised a 500 error instead of `ConsistencyError`. The server refuses such a PUT with 500 rather than 412, so `save.if-none-match` is now `ungraceful` there, and the library checks the name with a GET before the PUT instead of sending `If-None-Match: *`. diff --git a/changes/+todo-values-x-parameter-multiget-deleted.added.md b/changes/+todo-values-x-parameter-multiget-deleted.added.md index 98139c0e..64098a87 100644 --- a/changes/+todo-values-x-parameter-multiget-deleted.added.md +++ b/changes/+todo-values-x-parameter-multiget-deleted.added.md @@ -1 +1 @@ -New server hints: `save-load.todo.status` (RFC 5545's statuses), its child `save-load.todo.status.rfc10051` (PENDING and FAILED) and `save-load.todo.priority`, with the extra keys `values`, `map` and `refused` naming what a task keeps, stores as another, and is refused with; `supported-task-modes`, whether a server advertises RFC 10051 with `CALDAV:supported-task-mode-set`; `save-load.icalendar.x-parameter`; and `multiget.deleted` (Zimbra still returns a deleted object from a calendar-multiget). `save-load.stable-url` has the extra key `uid_ics_components`, for a server naming only some components `.ics` (Ox: tasks). The Ox and Zimbra profiles carry what the probes found. +New server hints: `save-load.todo.status` (RFC 5545's statuses), its child `save-load.todo.status.rfc10051` (PENDING and FAILED) and `save-load.todo.priority`, with the extra keys `values`, `map` and `refused` naming what a task keeps, stores as another, and is refused with; `supported-task-modes`, whether a server advertises RFC 10051 with `CALDAV:supported-task-mode-set`; `save-load.icalendar.x-parameter`; and `multiget.deleted` (Zimbra still returns a deleted object from a calendar-multiget). `save-load.stable-url` has the extra key `uid_ics_components`, for a server naming only some components `.ics` (Ox: tasks). The Ox, Zimbra and Bedework 3.10 profiles carry what the probes found. From c5f2223b5114cfd4c32db62fc42c4182a33cd62b Mon Sep 17 00:00:00 2001 From: Tobias Brox Date: Wed, 11 Nov 2026 19:19:01 +0100 Subject: [PATCH 2/2] fix: GMX refuses a task that is PENDING or FAILED GMX answers 403 to a task with STATUS:PENDING or STATUS:FAILED, the two values RFC 10051 adds, so save-load.todo.status.rfc10051 is "ungraceful" there, with both values refused. Probed with caldav-server-tester. Prompt: [pick from the task list: "calendaring: remaining conformance failures on Ox, Zimbra and others"] Followup-Prompt: ... First priority is to get the conformance tests passing for all the caldav servers available ... Assisted-By: Claude Opus 5.5 Reviewed-by: Tobias Brox --- caldav/compatibility_hints.py | 2 ++ changes/+todo-values-x-parameter-multiget-deleted.added.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/caldav/compatibility_hints.py b/caldav/compatibility_hints.py index 6ec7d59c..bc5486a4 100644 --- a/caldav/compatibility_hints.py +++ b/caldav/compatibility_hints.py @@ -3108,6 +3108,8 @@ def __getattr__(name: str) -> Any: "save-load.icalendar.olson-tzid": {"support": "full", "behaviour": "the server adds a VTIMEZONE for Asia/Tokyo"}, ## Probed 2026-11-07 by caldav-server-tester "search.time-range.todo.overlap": {"support": "unsupported", "behaviour": "only tasks starting inside the range are found"}, + ## Probed 2026-11-11 by caldav-server-tester + "save-load.todo.status.rfc10051": {"support": "ungraceful", "behaviour": "PENDING is refused (401/403); FAILED is refused (401/403); the server does not advertise RFC 10051", "values": [], "map": {}, "refused": ["PENDING", "FAILED"]}, } ## https://ox.io/ diff --git a/changes/+todo-values-x-parameter-multiget-deleted.added.md b/changes/+todo-values-x-parameter-multiget-deleted.added.md index 64098a87..8308b708 100644 --- a/changes/+todo-values-x-parameter-multiget-deleted.added.md +++ b/changes/+todo-values-x-parameter-multiget-deleted.added.md @@ -1 +1 @@ -New server hints: `save-load.todo.status` (RFC 5545's statuses), its child `save-load.todo.status.rfc10051` (PENDING and FAILED) and `save-load.todo.priority`, with the extra keys `values`, `map` and `refused` naming what a task keeps, stores as another, and is refused with; `supported-task-modes`, whether a server advertises RFC 10051 with `CALDAV:supported-task-mode-set`; `save-load.icalendar.x-parameter`; and `multiget.deleted` (Zimbra still returns a deleted object from a calendar-multiget). `save-load.stable-url` has the extra key `uid_ics_components`, for a server naming only some components `.ics` (Ox: tasks). The Ox, Zimbra and Bedework 3.10 profiles carry what the probes found. +New server hints: `save-load.todo.status` (RFC 5545's statuses), its child `save-load.todo.status.rfc10051` (PENDING and FAILED) and `save-load.todo.priority`, with the extra keys `values`, `map` and `refused` naming what a task keeps, stores as another, and is refused with; `supported-task-modes`, whether a server advertises RFC 10051 with `CALDAV:supported-task-mode-set`; `save-load.icalendar.x-parameter`; and `multiget.deleted` (Zimbra still returns a deleted object from a calendar-multiget). `save-load.stable-url` has the extra key `uid_ics_components`, for a server naming only some components `.ics` (Ox: tasks). The Ox, Zimbra, Bedework 3.10 and GMX profiles carry what the probes found.