From ebca04f341ecfc3e7be0104bf4c1f2290362052b Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Wed, 5 Aug 2026 10:05:48 +0200 Subject: [PATCH 1/4] Skip keyless update entries in updateSnapshots instead of crashing Onyx.update already skips entries without a string key (and clear/multiset entries legitimately have none), but passed the unfiltered array to updateSnapshots, which crashed on key.startsWith when a snapshot was cached. Fixes APP-6NT Co-Authored-By: Claude Fable 5 --- lib/OnyxUtils.ts | 6 ++++++ tests/unit/onyxTest.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 4dc1ba2ce..e23ff92eb 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -1211,6 +1211,12 @@ function updateSnapshots(data: Array>, me let updatedData: Record = {}; for (const {key, value} of data) { + // `clear`/`multiset` updates legitimately carry no key (and `Onyx.update` skips other keyless + // entries), so ignore them here too instead of crashing on `key.startsWith` below. + if (typeof key !== 'string') { + continue; + } + // snapshots are normal keys so we want to skip update if they are written to Onyx if (OnyxKeys.isCollectionMemberKey(snapshotCollectionKey, key)) { continue; diff --git a/tests/unit/onyxTest.ts b/tests/unit/onyxTest.ts index 0021fcd8e..9f4cfe5b8 100644 --- a/tests/unit/onyxTest.ts +++ b/tests/unit/onyxTest.ts @@ -1535,6 +1535,35 @@ describe('Onyx', () => { expect(callback.mock.calls[1][1]).toBe(ONYX_KEYS.COLLECTION.SNAPSHOT); }); + it('should skip update entries without a key when updating Snapshots instead of rejecting', async () => { + const cat = `${ONYX_KEYS.COLLECTION.ANIMALS}cat`; + const snapshot1 = `${ONYX_KEYS.COLLECTION.SNAPSHOT}1`; + + const initialValue = {name: 'Fluffy'}; + const finalValue = {name: 'Kitty'}; + + await Onyx.set(cat, initialValue); + await Onyx.set(snapshot1, {data: {[cat]: initialValue}}); + + const callback = jest.fn(); + + Onyx.connect({ + key: ONYX_KEYS.COLLECTION.SNAPSHOT, + callback, + }); + + await waitForPromisesToResolve(); + + // A keyless entry (e.g. a malformed server update) used to crash updateSnapshots with + // "can't access property 'startsWith', key is undefined" and reject the whole update. + const keylessUpdate = {onyxMethod: Onyx.METHOD.MERGE, value: {name: 'Ghost'}} as unknown as OnyxUpdate; + + await expect(Onyx.update([keylessUpdate, {key: cat, value: finalValue, onyxMethod: Onyx.METHOD.MERGE}])).resolves.not.toThrow(); + + // The valid update still lands in the snapshot. + expect(callback.mock.calls.at(-1)?.[0]).toEqual({[snapshot1]: {data: {[cat]: finalValue}}}); + }); + describe('update', () => { let logInfoFn = jest.fn(); From 81627b06ee8473977759cabc84c790a1d3ab907f Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Mon, 10 Aug 2026 15:16:38 +0200 Subject: [PATCH 2/4] Remove review-flagged comments --- lib/OnyxUtils.ts | 2 -- tests/unit/onyxTest.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index e23ff92eb..a5fbf7e1d 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -1211,8 +1211,6 @@ function updateSnapshots(data: Array>, me let updatedData: Record = {}; for (const {key, value} of data) { - // `clear`/`multiset` updates legitimately carry no key (and `Onyx.update` skips other keyless - // entries), so ignore them here too instead of crashing on `key.startsWith` below. if (typeof key !== 'string') { continue; } diff --git a/tests/unit/onyxTest.ts b/tests/unit/onyxTest.ts index 9f4cfe5b8..a606e6a83 100644 --- a/tests/unit/onyxTest.ts +++ b/tests/unit/onyxTest.ts @@ -1554,8 +1554,6 @@ describe('Onyx', () => { await waitForPromisesToResolve(); - // A keyless entry (e.g. a malformed server update) used to crash updateSnapshots with - // "can't access property 'startsWith', key is undefined" and reject the whole update. const keylessUpdate = {onyxMethod: Onyx.METHOD.MERGE, value: {name: 'Ghost'}} as unknown as OnyxUpdate; await expect(Onyx.update([keylessUpdate, {key: cat, value: finalValue, onyxMethod: Onyx.METHOD.MERGE}])).resolves.not.toThrow(); From 19dade93a3dc9b78310543575ea5eb19d140e3d9 Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Tue, 11 Aug 2026 14:40:11 +0200 Subject: [PATCH 3/4] Log skipped keyless update entries in updateSnapshots --- lib/OnyxUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index a5fbf7e1d..6cbd61d7d 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -1212,6 +1212,7 @@ function updateSnapshots(data: Array>, me for (const {key, value} of data) { if (typeof key !== 'string') { + Logger.logInfo(`Invalid ${typeof key} key provided in Onyx update. Key must be of type string. Skipping snapshot update for this entry.`); continue; } From 758d11fc5a49b14416a7fff75c36b4336e22a17d Mon Sep 17 00:00:00 2001 From: eliran goshen Date: Tue, 11 Aug 2026 16:17:53 +0200 Subject: [PATCH 4/4] Include onyxMethod and key in skipped-entry log and stay silent for clear/multiset --- lib/OnyxUtils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 6cbd61d7d..5123e7775 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -1210,9 +1210,12 @@ function updateSnapshots(data: Array>, me let updatedData: Record = {}; - for (const {key, value} of data) { + for (const {key, value, onyxMethod} of data) { if (typeof key !== 'string') { - Logger.logInfo(`Invalid ${typeof key} key provided in Onyx update. Key must be of type string. Skipping snapshot update for this entry.`); + // clear/multiset entries legitimately carry no key; snapshots have nothing to update for them + if (onyxMethod !== METHOD.CLEAR && onyxMethod !== METHOD.MULTI_SET) { + Logger.logInfo(`Invalid ${typeof key} key (method: ${onyxMethod}, key: ${String(key).slice(0, 50)}) provided in Onyx update. Skipping snapshot update for this entry.`); + } continue; }