pimutils / pimutils/vdirsyncer
google_contacts: _put records request URL instead of Location header → deletes/re-uploads its own contacts on every sync
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.9k
- Forks
- 185
- Avg merge
- 11h 49m
- Merged PRs (30d)
- 1
Description
Sorry for AI written bug report, but i failed quite early on troubleshooting that on my own.
Environment:
- vdirsyncer 0.20.0
- Python 3.12, aiohttp
- Running via the bleala/vdirsyncer:2.6.1 Docker image (Bleala/Vdirsyncer-DOCKERIZED (https://github.com/Bleala/Vdirsyncer-DOCKERIZED), Alpine 3.24) — but the bug is in upstream vdirsyncer code, reproducible independent of
the container - Target: Google Contacts CardDAV (type = "google_contacts")
Summary
Syncing a carddav source (read-only) to a google_contacts target never converges: on every run vdirsyncer re-uploads all items, deletes contacts it previously created on Google, and most uploads fail with 400 "Request
contains an invalid argument." The address book stays empty and the sync loops forever.
Root cause
Google CardDAV ignores the client-supplied resource name on PUT and reassigns its own href, returning it in the Location header:
PUT .../lists/default/.vcf → 201 Created
Location: .../lists/default/29935b729362b1b1
DAVStorage._put records the request URL rather than the Location:
etag = response.headers.get("etag", None)
href = self._normalize_href(str(response.url)) # .vcf, but Google stored it at 29935b...
return href, etag
So the stored href never matches what list() returns next run (29935b…). vdirsyncer then treats its own uploaded contacts as unknown items on the writable side and (with a read-only source) deletes them, and re-uploads the
source copy. The re-upload PUT to .vcf for a now-deleted UID returns 400 — Google permanently tombstones a deleted contact's UID-href — so the sync is stuck.
Expected: vdirsyncer records the server-assigned href from Location, recognizes its uploads on subsequent runs, and makes no spurious deletes/re-uploads.
Actual: endless upload→delete→re-upload churn; 400s once UIDs are tombstoned.
Suggested fix — prefer the Location header when present (RFC 7231 §7.1.2 / §6.3.2: a 201 SHOULD carry Location):
etag = response.headers.get("etag", None)
location = response.headers.get("Location")
href = self._normalize_href(location or str(response.url))
return href, etag
Reproduce: one-way carddav (read-only) → google_contacts pair, sync twice; observe re-uploads + Deleting item … from … + 400s. A raw PUT to Google returns 201 with a Location that differs from the request path, and a
follow-up PROPFIND lists the contact only under the Location href.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at DAVStorage._put, where the issue identifies response.url as the stored href, and inspect how the returned href is used by subsequent syncs. Reproduce with a read-only CardDAV source and google_contacts target, then verify that using the server-provided Location href prevents deletes and re-uploads on the second sync.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100