Writing a card or an event: a server that answers a PUT without an ETag makes the plugin list the whole collection again #46
Labels
No labels
bug
documentation
enhancement
performance
security
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo-dav#46
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What exists today
src/postulo_dav/dav.py:383-396Server.put: when the answer to aPUTcarries noETag, line 395 callsself.etags(href.rsplit("/", 1)[0] + "/"), a Depth 1PROPFINDof the whole collection (dav.py:347-375), to pick out one resource's ETag. Every caller goes through it: a new card (sync.py:797), a changed card (sync.py:895), a new or changed event (sync.py:1050,1061). So pushing N records costs N full listings of a collection that grows by one each time.When does a server omit the ETag? RFC 4791 §5.3.4 and RFC 6352 §6.3.2.3 say a server MUST NOT return a strong ETag when the stored object is not byte-for-byte what was sent. SabreDAV (Nextcloud, Baïkal) drops the header whenever its validator repairs or rewrites the body, and the plugin's first try at an event carries
METHOD:PUBLISH(sync.py:1046-1056), which RFC 4791 §4.1 forbids in a stored object; a server that accepts the event and strips the line has changed it, and must not answer with an ETag. So the fallback is the common path for events on lenient servers, not an edge case.The fallback is not exercised by the tests: the fake server (
tests/test_dav.py:45-120) always answers aPUTwith anETag.What would change
Ask for the one resource: a Depth 0
PROPFINDofhrefforgetetag(the existing_ETAGSbody, throughpropfind(href, _ETAGS, "0")), read with the same href checks, so a write costs one small request whatever the size of the collection. If that answer has no ETag either, store""as today; the next run then sees a changed ETag, reads the resource once and settles it (sync.py:827-833).A test: the fake server gains a switch to answer
PUTwithoutETag, and pushing three contacts asserts no Depth 1PROPFINDafter the first listing of the run.What it saves or protects
medium-large on the servers concerned: a first push of 300 contacts lists the collection 300 times (about 45,000 listing entries, some 9 MB of XML parsed; computed from the code, not measured); one small request per write instead
Accessibility
none
Decisions
put()self-contained, and the conflict handling that follows a write already expects the ETag at once.Found by the efficiency and reliability audit of 2026-10-30 (finding PR6; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).