Interviews: on a server that percent-encodes its hrefs (Radicale), every event is flagged removed on the second run #19
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#19
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?
Found on 2026-09-29 in a read-only review of postulo-dav at
97abc9779, reading the sync and running it against the test suite's fake server with its listing hrefs percent-encoded.An interview's event is written to an address built from the interview's UID, and that literal string is stored on the link. The UID always contains an
@. A server that percent-encodes the hrefs it lists, as Radicale does, lists the same event with%40, and the plugin compares the two strings as they are. On the second run every interview looks deleted from the calendar: it is flagged removed, a false entry goes on the application's timeline, and the interview is never pushed or pulled again. Radicale is one of the servers the README names.src/postulo_dav/sync.py:576(_sync_interviews,href = urljoin(calendar.href, f"{interview.uid}.ics"), stored asremote_hrefat:581);src/postulo_dav/sync.py:589-601(if link.remote_href not in remote:flagsremote_goneand callsrecord_event);src/postulo_dav/sync.py:667-673(the same resource counted as foreign);src/postulo_dav/dav.py:204-221(Server.etags, which keys its result byself.absolute(href_node.text.strip())and usesunquoteonly to skip the collection itself).<uuid4>@postulo(applications/models.py:694in the core). Run 1 PUTs…/postulo-interviews/<uuid>@postulo.icsand stores that string.urllib.parse.quote, whose default safe set is/, so the PROPFIND answer says…/<uuid>%40postulo.ics.remote, so the link getsremote_gone = True, the report says "1 removed, 1 skipped · 1 events on the calendar are not from Postulo and were left alone", and the timeline gains "Panel removed from the calendar on another device; kept here."if link.remote_gone: continue(:569-570) skips the interview: a time moved here never reaches the phone, and one moved on the phone never reaches Postulo.Reproduced with the
FakeDavfromtests/test_dav.py, changed only so that_multistatuspasses each href throughquote(unquote(href)). Contacts escape only because their resource names are bareuuid4values (:441).dav.py(decode the path withunquoteand re-encode it with one fixed safe set, or keep decoded paths throughout) and apply it to the keysetags()returns, to every href stored on a link and to every lookup against them. Independently, build new resource names from a filename-safe form of the UID (for example the UUID part alone) so the address carries no reserved characters to begin with; existing links keep working once comparison is canonical.Related to #7, which asks that a twin missing from the listing be looked up by its UID before it is called gone; that would hide this case but not fix the comparison.
Done when
FakeDavvariant intests/test_dav.pythat percent-encodes its listing hrefs, and a test there that schedules an interview, syncs twice and asserts the link is notremote_gone, the second run reports nothing to do, and no "removed from the calendar" entry is on the timeline.