CardDAV PUT on a JMAP-created contact returns 412 no-uid-conflict on the same href (UID urn:uuid: vs bare UUID)

Issue Description

Updating a contact that was created via JMAP (ContactCard/set) from a CardDAV client (DAVx5) fails with HTTP 412 and CARDDAV:no-uid-conflict. The href in the error is the same resource being PUT. Contacts created via CardDAV can be updated via CardDAV without this error.

JMAP stores the card UID as urn:uuid:. DAVx5 sends UID: without the urn:uuid: prefix. It looks like the server treats those as two different UIDs and reports a conflict against the existing JMAP card (or against the same href).

Expected Behavior

PUT updates the existing resource (200/204), UID treated as the same person whether or not it is prefixed with urn:uuid:.

Actual Behavior

Stalwart return 412 when trying to update an existing contact.

Reproduction Steps

  1. Create a contact in a JMAP client (ContactCard/set) in the account’s default address book.

  2. Confirm JMAP ContactCard/get shows something like:
    id: ba
    uid: urn:uuid:cddccf70-dc55-4cad-a171-f55f1c5c0162
    addressBookIds: { “b”: true }

  3. Sync the same address book with DAVx5 (/.well-known/carddav → /dav/card//default).

  4. Edit the contact on the phone (title/org) and sync.
    DAVx5 PUTs to the discovered href with If-Match and a vCard 4 body whose UID is the bare UUID.

Relevant Log Output

Request:

PUT /dav/card/[email protected]/default/nNhLyd8baW
If-Match: "3496115365"
Content-Type: text/vcard; version=4.0

BEGIN:VCARD
VERSION:4.0
PRODID:+//IDN bitfire.at//DAVx5/4.5.19-ose ez-vcard/0.12.2
UID:cddccf70-dc55-4cad-a171-f55f1c5c0162
FN:John Doe
N:Doe;John;;;
ORG:ACME Inc;TechnicalDept
TITLE:Engineer
TEL;TYPE=cell:+156236549
GRAMGENDER:MASCULINE
REV:20260917T104932Z
END:VCARD

Response:

HTTP/1.1 412 Precondition Failed
Content-Type: application/xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<D:error xmlns:D="DAV:" xmlns:B="urn:ietf:params:xml:ns:carddav">
  <B:no-uid-conflict>
    <D:href>/dav/card/user%40example.com/default/nNhLyd8baW</D:href>
  </B:no-uid-conflict>
</D:error>

DAVx5 then logs: Resource has been modified on the server before upload, ignoring. The edit never lands. JMAP clients keep showing the old card.

The conflict href is the PUT target itself, not a second file.

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

PostgreSQL

Blob Storage

PostgreSQL

Search Engine

PostgreSQL

Directory Backend

Internal

Additional Context

Control case (works)

A contact created on DAVx5 (UID minted by DAVx5, no urn:uuid: prefix) can be edited on the phone and the change appears in JMAP ContactCard/get. So CardDAV update in general works; the failure is specific to cards that originated from JMAP.

A second JMAP-created contact (different UID/href) failed the same way on PUT (412 + no-uid-conflict).

Notes:

RFC 6352 no-uid-conflict is for “this UID is already used by a different resource” or “overwrite would change the UID”. Same-href update with an equivalent UID should not hit it.

RFC 6350 UID may be a URN. Clients often omit urn:uuid:. Servers usually normalize before uniqueness checks.

Access log often records only the incoming PUT body (HTTP still 412). The condition is in the XML body, not a JMAP methodResponses error.

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

Your diagnosis matches what the CardDAV update code actually does. On PUT, Stalwart compares the vCard UID of the existing stored card against the UID in the incoming vCard as plain strings, and rejects the update with no-uid-conflict unless they’re identical:

match (card.inner.card.uid(), vcard.uid()) {
    (Some(old_uid), Some(new_uid)) if old_uid == new_uid => {}
    (None, None) | (None, Some(_)) => {}
    _ => {
        return Err(DavError::Condition(DavErrorCondition::new(
            StatusCode::PRECONDITION_FAILED,
            CardCondition::NoUidConflict(...),
        )));
    }
}

(server/crates/dav/src/card/update.rs). There is no normalization step that strips a urn:uuid: prefix before the comparison, so a card whose stored UID is urn:uuid:<uuid> (as JMAP ContactCard/set writes it) will never equal an incoming bare <uuid> from a CardDAV client like DAVx5, even though RFC 6350 treats them as the same identifier. This is exactly the same-href, same-person case you described, so the 412 is a genuine bug in the UID equality check rather than expected behavior.

I can’t fix code from here, so this will need a maintainer/developer to address the comparison logic (e.g. normalizing both sides by stripping the urn:uuid: scheme, or comparing canonical UUIDs when applicable) before this case stops producing false conflicts.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

This has been fixed in the v1.0.0 branch as it could not be fixed in v0.16 branch without an index migration. v1.0 is expected at the end of October or early November.

Update: The bot got this one wrong. RFC 6350 does not treat urn:uuid:X and a bare X as the same UID, and the 412 is Stalwart applying RFC 6352, not a broken comparison.

Stalwart keeps the UID as your JMAP client created it (urn:uuid: is the form RFC 9553 recommends for JSContact) and serves it to DAVx5 unchanged. DAVx5 strips the prefix when it imports the contact and uploads the bare UUID when you edit it, so every PUT from the phone tries to change the UID of an existing card, and RFC 6352 section 6.3.2.1 requires the server to refuse that with no-uid-conflict. A same-href update with an equivalent UID would go through, but these two are not equivalent: RFC 6350 section 7.1.1 compares UIDs as URIs, and a bare UUID has no scheme, so it is not a URI at all. Contacts created on the phone work because DAVx5 minted those UIDs without the prefix.

DAVx5 does this on purpose, so the v1.0.0 branch now accepts these edits. When the UID in a PUT differs from the stored one only by the urn:uuid: prefix or the case of the UUID, Stalwart saves the edit and keeps the UID it already had. That response has no ETag, since the stored card is no longer byte for byte what DAVx5 sent, and DAVx5 fetches the card again on its next sync. A different UUID still gets the 412.

Nothing needs configuring once you are on 1.0. Until then, edit those contacts from the JMAP client. I haven’t checked whether DAVx5 keeps rejected edits queued or discards them, so look over anything you changed on the phone after upgrading.

Please report the stripping to DAVx5 as well, in their GitHub discussions (bitfireAT/davx5-ose · Discussions · GitHub). Any server that enforces RFC 6352 will reject these edits, and DAVx5 reports a no-uid-conflict 412 as “Resource has been modified on the server before upload”, which hides the real reason.