From dbaa4fceaa9d3435098bedbd487b577d0d56b409 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 21 Aug 2026 17:46:01 -0700 Subject: [PATCH 1/2] fix(storage): serialize Error.cause when degrading Avoid interpolating a raw cause object into the fallback-to-cache alert so logs stay readable. --- lib/storage/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/storage/index.ts b/lib/storage/index.ts index b4e80e74c..0b15ca663 100644 --- a/lib/storage/index.ts +++ b/lib/storage/index.ts @@ -20,7 +20,8 @@ type Storage = { * Degrade performance by removing the storage provider and only using cache */ function degradePerformance(error: Error) { - Logger.logHmmm(`Error while using ${provider.name}. Falling back to only using cache and dropping storage.\n Error: ${error.message}\n Stack: ${error.stack}\n Cause: ${error.cause}`); + const causeMessage = error.cause instanceof Error ? error.cause.message : typeof error.cause === 'string' ? error.cause : ''; + Logger.logHmmm(`Error while using ${provider.name}. Falling back to only using cache and dropping storage.\n Error: ${error.message}\n Stack: ${error.stack}\n Cause: ${causeMessage}`); console.error(error); provider = MemoryOnlyProvider; } From 698a3d963afb89ca411840f4a7633487f0ba886e Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 21 Aug 2026 22:01:38 -0700 Subject: [PATCH 2/2] fix: flatten cause serialization and cover it Avoid a nested ternary in degradePerformance and assert the Cause field is a readable string, not [object Object]. --- lib/storage/index.ts | 7 ++++++- .../storage/tryOrDegradePerformanceTest.ts | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/storage/index.ts b/lib/storage/index.ts index 0b15ca663..5c6313fff 100644 --- a/lib/storage/index.ts +++ b/lib/storage/index.ts @@ -20,7 +20,12 @@ type Storage = { * Degrade performance by removing the storage provider and only using cache */ function degradePerformance(error: Error) { - const causeMessage = error.cause instanceof Error ? error.cause.message : typeof error.cause === 'string' ? error.cause : ''; + let causeMessage = ''; + if (error.cause instanceof Error) { + causeMessage = error.cause.message; + } else if (typeof error.cause === 'string') { + causeMessage = error.cause; + } Logger.logHmmm(`Error while using ${provider.name}. Falling back to only using cache and dropping storage.\n Error: ${error.message}\n Stack: ${error.stack}\n Cause: ${causeMessage}`); console.error(error); provider = MemoryOnlyProvider; diff --git a/tests/unit/storage/tryOrDegradePerformanceTest.ts b/tests/unit/storage/tryOrDegradePerformanceTest.ts index b160b5903..de6f8c5a3 100644 --- a/tests/unit/storage/tryOrDegradePerformanceTest.ts +++ b/tests/unit/storage/tryOrDegradePerformanceTest.ts @@ -69,6 +69,24 @@ describe('storage/tryOrDegradePerformance', () => { expect(storage.getStorageProvider().name).toBe('MemoryOnlyProvider'); }); + it('serializes Error.cause as a readable string in the degrade log', async () => { + const {storage, Logger} = loadIsolatedStorage(); + const capturedLogs: CapturedLog[] = []; + Logger.registerLogger((data: LogData) => capturedLogs.push({level: data.level, message: data.message})); + + storage.init(); + + const originalProvider = storage.getStorageProvider(); + const targetError = new Error('IDBKeyVal store could not be created', {cause: new Error('underlying disk is full')}); + originalProvider.getAllKeys = jest.fn().mockReturnValue(Promise.reject(targetError)); + + await expect(storage.getAllKeys()).rejects.toBe(targetError); + + const degradeLog = capturedLogs.find((log) => log.level === 'hmmm' && log.message.includes('Falling back to only using cache')); + expect(degradeLog?.message).toContain('Cause: underlying disk is full'); + expect(degradeLog?.message).not.toContain('[object Object]'); + }); + it('propagates async rejections with unrelated messages without falling back', async () => { const {storage, Logger} = loadIsolatedStorage(); const capturedLogs: CapturedLog[] = [];