notify: only "the app is gone" deletes a device row
reported from the field on the first real push: it failed with BadDeviceToken and the server deleted the registration. the build was production-signed and the row said sandbox — a configuration mismatch, with the device and the app both perfectly fine. pruning there destroys the evidence. the phone re-registers on next launch, the row comes back looking healthy, and the next send fails identically; you can lose a day to that loop without ever seeing what is actually wrong. so the one set becomes three. Unregistered/UNREGISTERED/NOT_FOUND prune — apple and google are saying the app is uninstalled. BadDeviceToken, DeviceTokenNotForTopic, TopicDisallowed, INVALID_ARGUMENT and SENDER_ID_MISMATCH are OURS: the row is kept and the log says what to check. everything else is transient and still counts toward three strikes. INVALID_ARGUMENT was the sharpest one. it is how fcm reports a malformed MESSAGE — including a non-string in `data`, which is the mistake this codebase is most likely to make and already has a test for. deleting on it meant one bug in the payload builder would wipe every android device from the registry on a single send, presenting as "android push stopped working" with nothing left to explain why. misconfigured is also not counted as a strike. not deleting is not enough on its own — a persistent mismatch would reach three failures and delete the row anyway, which is the same bug with a slower fuse. the apns log now names the mismatch rather than the reason code, since the code alone sends people to look at the device: it prints the row's environment and topic and says to check the SHIPPED artefact rather than the intermediate build, which is exactly where this one hid.
This commit is contained in:
@@ -275,11 +275,28 @@ tokens rotate.
|
||||
|
||||
## Failure handling
|
||||
|
||||
**APNs** answers inline: `410 Unregistered` or `400 BadDeviceToken` means delete the row immediately.
|
||||
**FCM** likewise: `UNREGISTERED` / `INVALID_ARGUMENT` / `NOT_FOUND` means delete.
|
||||
**Only "the app is gone" prunes a row.** The first version pruned on every 4xx Apple or Google offered,
|
||||
and that was wrong in a way that hides itself.
|
||||
|
||||
Everything else increments `failure_count`; three strikes and the row goes. Without this the registry fills
|
||||
with dead tokens and delivery quietly degrades.
|
||||
| reason | what it means | what happens |
|
||||
| ------------------------------------------------------------- | ------------------------------- | ---------------------------------------------------- |
|
||||
| `Unregistered` (410), `UNREGISTERED`, `NOT_FOUND` | the app was uninstalled | **delete the row** |
|
||||
| `BadDeviceToken`, `DeviceTokenNotForTopic`, `TopicDisallowed` | **our** configuration is wrong | keep it, log the diagnosis |
|
||||
| `INVALID_ARGUMENT`, `SENDER_ID_MISMATCH` | **we** sent something malformed | keep it, log the diagnosis |
|
||||
| anything else | transient | `failure_count += 1`, three strikes and the row goes |
|
||||
|
||||
`BadDeviceToken` is almost always an environment mismatch — a token minted under
|
||||
`aps-environment: development` sent to the production host, or the reverse. The device is fine, the app is
|
||||
installed, the registration is good; the server dialled the wrong Apple. **Deleting the row there destroys
|
||||
the evidence**: the phone re-registers on next launch, the row comes back looking healthy, and the next
|
||||
send fails identically. Found in the field on the first real push, 2026-08-19.
|
||||
|
||||
`INVALID_ARGUMENT` is worse still, because it is how FCM reports a malformed MESSAGE — including a
|
||||
non-string in `data`, the mistake this codebase is most likely to make and has a test for. Pruning on it
|
||||
means one bug in the payload builder wipes every Android device in the registry on a single send.
|
||||
|
||||
**Misconfigured is not counted as a strike either.** Not deleting is not enough on its own: a persistent
|
||||
mismatch would otherwise reach three failures and delete the row anyway. Same bug, slower fuse.
|
||||
|
||||
## Android and GrapheneOS — UNRESOLVED, and it decides Android
|
||||
|
||||
|
||||
Reference in New Issue
Block a user