firebase / firebase/firebase-ios-sdk
Firestore: unacked writes replayed on reconnect, double-applying FieldValue.increment (see firebase-android-sdk#8440)
- Dominant language
- C++
- Stars
- 6.7k
- Forks
- 1.8k
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 75
Description
### Description
This is a cross-SDK tracker for a Firestore defect that has already been confirmed and root-caused on Android in firebase/firebase-android-sdk#8440. The cause described there is not Android-specific, so I'm filing here so the iOS SDK isn't missed when it's addressed.
Root cause, as described by the Android SDK team ([comment](https://github.com/firebase/firebase-android-sdk/issues/8440#issuecomment-5122923991)):
1. The backend (`/google.firestore.v1.Firestore/Write`) does not currently support write stream resumption across connections (`stream_token`).
2. If a connection drops after the server commits a write batch but before the ACK reaches the client, the client re-sends the unacknowledged batch on reconnect, and the server — unable to resume the previous stream session — executes it a second time.
3. Replaying an idempotent write (e.g. `set(v: 5)`) is harmless, but replaying a relative transform like `FieldValue.increment(-1)` applies the delta twice. Nothing fails: the completion handler reports success while client and server silently diverge.
Since the trigger is a backend limitation combined with the ordinary client behavior of re-sending unacknowledged batches, any client SDK that queues offline mutations should be affected — this one included.
**Expected:** each queued mutation is applied exactly once when connectivity returns.
**Actual:** a batch that was committed but not acknowledged before a disconnect is applied twice; `increment` counters drift by one delta per occurrence, with no error surfaced.
### Reproducing the issue
The original report ([firebase/flutterfire#12952](https://github.com/firebase/flutterfire/issues/12952)) reproduced on **both iOS and Android** through FlutterFire, which only forwards `increment` / `disableNetwork` / `enableNetwork` to the native SDKs. A FlutterFire maintainer then reduced it to a [pure native Android project](https://github.com/TarekkMA/firebase-android-reproduction/tree/ff-issue/12952), and the FlutterFire issue was closed as an upstream native SDK bug.
I don't have a pure-Swift reproduction — the iOS observation came through FlutterFire. The equivalent sequence:
```swift
let db = Firestore.firestore()
let doc = db.collection("test").document("doc") // field "v" starts at 0
doc.updateData(["v": FieldValue.increment(Int64(-1))]) // not awaited, as in real offline-capable apps
db.disableNetwork()
doc.updateData(["v": FieldValue.increment(Int64(1))])
db.enableNetwork()
```
Repeat every 2–3 seconds (or fire several sequences concurrently to make it near-deterministic) while watching the document with a snapshot listener. `v` should return to `0` after each `-1`/`+1` pair; instead it drifts negative, consistent with the `-1` batch being applied twice.
`disableNetwork()` / `enableNetwork()` are only a deterministic stand-in for a flaky real-world connection — the original report saw the same drift on a physical device with a genuinely unstable network.
### References
- Android issue with the team's root-cause analysis: firebase/firebase-android-sdk#8440
- Internal tracking for the backend write-stream-resumption / mutation-dedup work: **b/540459641** — per [this comment](https://github.com/firebase/firebase-android-sdk/issues/8440#issuecomment-5132437408), #8440 is linked to it and is the public reference to follow
- Original FlutterFire report (reproduced on iOS and Android): firebase/flutterfire#12952
Contributor guide
Assessment
This issue has not been assessed yet.