π» Platform
βΉοΈ Environment
- App Version: Android: current Play Store build as of 2026-09-01 13:18 UTC. v1.2.6 was published 12:56 UTC the same day, so this is almost certainly 1.2.5. (The
.mindwtr-sync-fence-v1.json behaviour implies >= 1.2.5.)
- Install Channel(s): Android: Play Store. Desktop: same account, syncing successfully against the identical endpoint at the same time.
- Sync Backend: WebDAV (self-hosted
wsgidav 4.3.5 behind Caddy 2.11.4, plaintext, encryption off)
- OS Version(s): Android (version not recorded); server Ubuntu 24.04
- Device clocks auto-sync enabled: yes β phone measured ~1.4 s behind the server, which I have corrected for when aligning the two logs below.
Host redacted as https://example.com/dav throughout.
π§© What happened?
Android sync fails on every attempt at step read-remote:
Remote sync mutation fence record is malformed
Desktop syncs the same data.json at the same endpoint continuously and without error.
The Android client never requests the fence file at all. I have server-side access logs for the whole session. Counting every request to .mindwtr-sync-fence-v1.json:
| Client (User-Agent) |
Requests to .mindwtr-sync-fence-v1.json |
tauri-plugin-http/2.5 (desktop) |
27 β GETβ404, PUTβ201, GETβ200 xN, DELETEβ204. Correct lease cycle. |
okhttp/4.12.0 (Android) |
0 |
Instead, at the exact moment of the fence read, Android issues GET /dav/data.json. Aligning the two logs (phone ~1.4 s behind):
| Android app log |
Server access log |
13:18:32.576 Sync step {"step":"read-remote"} |
β |
| β |
13:18:33.940 GET /dav/data.json β 200 (3360 bytes) |
13:18:32.673 Sync failed: Remote sync mutation fence record is malformed |
β |
So the fence URL appears to resolve back to the document URL on Android. parseRecord then receives data.json β syntactically valid JSON, but with no schema / leaseId / ownerId / purpose / expiresAt β and correctly throws at the field check in packages/core/src/sync-remote-fence.ts.
This looks like the same class of defect already noted in packages/core/src/webdav.ts:
A short-lived Android URL fallback regression could serialize the unique probe URL back to data.json.
i.e. the same URL-resolution fallback, now affecting the fence path rather than the probe path. Note the fence filename begins with a dot, which is a plausible trigger for a URL-resolution fallback.
This is probably not fixed by v1.2.6. f24d80ad treats an oversized fence response as absence; here the fence is never fetched, so size never comes into it. My server's 404 body is 408 bytes, well under the 4096 limit from #1126.
π Reproduction Steps
- Point Desktop and Android at the same plaintext WebDAV folder, encryption off.
- Let Desktop sync so
data.json exists and is valid.
- On Android, run Test Connection β it succeeds (the full capability probe passes; see below).
- Press Sync.
- Sync fails immediately with
Remote sync mutation fence record is malformed; the UI shows settings.syncFailureGeneric ("Review Settings β Sync and try again") and Last sync: Never.
Reproduces on every attempt β 5+ consecutive tries, plus repeats after app restart.
β
Expected result
Android acquires the fence at <folder>/.mindwtr-sync-fence-v1.json (creating it when absent, exactly as Desktop does) and completes the sync.
π Diagnostic data
Android log (verbatim, two consecutive attempts):
{"ts":"2026-09-01T13:18:32.302Z","level":"info","scope":"sync","message":"Sync start","context":{"backend":"webdav"}}
{"ts":"2026-09-01T13:18:32.464Z","level":"info","scope":"sync","message":"Sync step","context":{"step":"read-local","sinceLastStepMs":"144"}}
{"ts":"2026-09-01T13:18:32.561Z","level":"info","scope":"sync","message":"Sync local reconcile","context":{"reconcile":"aligned-skip","durationMs":"2","tasks":"13"}}
{"ts":"2026-09-01T13:18:32.576Z","level":"info","scope":"sync","message":"Sync step","context":{"step":"read-remote","sinceLastStepMs":"113"}}
{"ts":"2026-09-01T13:18:32.673Z","level":"warn","scope":"sync","message":"Sync failed","context":{"error":"Remote sync mutation fence record is malformed"}}
{"ts":"2026-09-01T13:18:36.488Z","level":"info","scope":"sync","message":"Sync start","context":{"backend":"webdav"}}
{"ts":"2026-09-01T13:18:36.703Z","level":"info","scope":"sync","message":"Sync step","context":{"step":"read-remote","sinceLastStepMs":"121"}}
{"ts":"2026-09-01T13:18:36.784Z","level":"warn","scope":"sync","message":"Sync failed","context":{"error":"Remote sync mutation fence record is malformed"}}
Server access log β the Android capability probe, which passes in full. Every assertion in assertWebdavConditionalWriteSupport is satisfied:
GET data.json -> 200 3360b
PUT data.json.mindwtr-etag-probe-... -> 201 (If-None-Match: *, create)
GET data.json.mindwtr-etag-probe-... -> 200
PUT data.json.mindwtr-etag-probe-... -> 412 (If-None-Match: * on existing, refused)
PUT data.json.mindwtr-etag-probe-... -> 204 (If-Match: current, replaced; ETag changes)
GET data.json.mindwtr-etag-probe-... -> 200
PUT data.json.mindwtr-etag-probe-... -> 412 (If-Match: stale, refused)
DELETE data.json.mindwtr-etag-probe-... -> 412 (If-Match: stale, refused)
GET data.json.mindwtr-etag-probe-... -> 200 (ETag unchanged after refused delete)
DELETE data.json.mindwtr-etag-probe-... -> 204 (If-Match: current)
No fence request appears anywhere in that sequence, or anywhere else in the session.
Server behaviour, for completeness:
- Strong ETags are a SHA-256 of file content (64 hex, quoted), so they change iff content changes.
GET of a missing file β 404 with a 408-byte text/html body, Content-Type: text/html; charset=utf-8.
OPTIONS β DAV: 1,2.
- Conditional semantics verified independently with curl:
If-Match mismatch β 412 with the write prevented, If-None-Match: * on an existing file β 412, conditional GET β 304.
- No compression anywhere in the chain; Android's
Accept-Encoding: identity is honoured.
Reproduces: every sync, 100%.
Possibly related
π» Platform
βΉοΈ Environment
.mindwtr-sync-fence-v1.jsonbehaviour implies >= 1.2.5.)wsgidav4.3.5 behind Caddy 2.11.4, plaintext, encryption off)Host redacted as
https://example.com/davthroughout.π§© What happened?
Android sync fails on every attempt at step
read-remote:Desktop syncs the same
data.jsonat the same endpoint continuously and without error.The Android client never requests the fence file at all. I have server-side access logs for the whole session. Counting every request to
.mindwtr-sync-fence-v1.json:.mindwtr-sync-fence-v1.jsontauri-plugin-http/2.5(desktop)GETβ404,PUTβ201,GETβ200 xN,DELETEβ204. Correct lease cycle.okhttp/4.12.0(Android)Instead, at the exact moment of the fence read, Android issues
GET /dav/data.json. Aligning the two logs (phone ~1.4 s behind):13:18:32.576 Sync step {"step":"read-remote"}13:18:33.940 GET /dav/data.json β 200 (3360 bytes)13:18:32.673 Sync failed: Remote sync mutation fence record is malformedSo the fence URL appears to resolve back to the document URL on Android.
parseRecordthen receivesdata.jsonβ syntactically valid JSON, but with noschema/leaseId/ownerId/purpose/expiresAtβ and correctly throws at the field check inpackages/core/src/sync-remote-fence.ts.This looks like the same class of defect already noted in
packages/core/src/webdav.ts:i.e. the same URL-resolution fallback, now affecting the fence path rather than the probe path. Note the fence filename begins with a dot, which is a plausible trigger for a URL-resolution fallback.
This is probably not fixed by v1.2.6.
f24d80adtreats an oversized fence response as absence; here the fence is never fetched, so size never comes into it. My server's 404 body is 408 bytes, well under the 4096 limit from #1126.π Reproduction Steps
data.jsonexists and is valid.Remote sync mutation fence record is malformed; the UI showssettings.syncFailureGeneric("Review Settings β Sync and try again") and Last sync: Never.Reproduces on every attempt β 5+ consecutive tries, plus repeats after app restart.
β Expected result
Android acquires the fence at
<folder>/.mindwtr-sync-fence-v1.json(creating it when absent, exactly as Desktop does) and completes the sync.π Diagnostic data
Android log (verbatim, two consecutive attempts):
Server access log β the Android capability probe, which passes in full. Every assertion in
assertWebdavConditionalWriteSupportis satisfied:No fence request appears anywhere in that sequence, or anywhere else in the session.
Server behaviour, for completeness:
GETof a missing file β404with a 408-bytetext/htmlbody,Content-Type: text/html; charset=utf-8.OPTIONSβDAV: 1,2.If-Matchmismatch β 412 with the write prevented,If-None-Match: *on an existing file β 412, conditionalGETβ 304.Accept-Encoding: identityis honoured.Reproduces: every sync, 100%.
Possibly related