pimutils / pimutils/vdirsyncer

google_contacts: _put records request URL instead of Location header → deletes/re-uploads its own contacts on every sync

Open Beginner friendly
#1,223 2 comments 0 reactions 0 assignees View on GitHub

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.