diff --git a/lib/storage/errors.ts b/lib/storage/errors.ts index 631b01535..1a03b4eeb 100644 --- a/lib/storage/errors.ts +++ b/lib/storage/errors.ts @@ -28,15 +28,37 @@ const StorageErrorClass = { UNKNOWN: 'unknown', } as const; +/** + * Serializes non-Error thrown values for classifier matching. JSON.stringify is preferred for plain + * objects, but it throws on circular structures and returns undefined for BigInt/function/symbol, so + * fall back to String() to keep normalization from throwing before classifyError runs. + */ +function serializeThrownValue(error: unknown): string { + try { + return JSON.stringify(error) ?? String(error); + } catch { + return String(error); + } +} + /** * Normalizes any thrown value into a lowercased `{name, message}` pair for matching. Shared by every * provider's classifier so they all extract the error the same way. */ function getErrorParts(error: unknown): {name: string; message: string} { if (error instanceof Error || (typeof DOMException !== 'undefined' && error instanceof DOMException)) { - return {name: (error.name ?? '').toLowerCase(), message: (error.message ?? '').toLowerCase()}; + return { + name: (error.name ?? '').toLowerCase(), + message: (error.message ?? '').toLowerCase(), + }; + } + if (typeof error === 'string') { + return {name: '', message: error.toLowerCase()}; + } + if (error === null || error === undefined) { + return {name: '', message: ''}; } - return {name: '', message: String(error ?? '').toLowerCase()}; + return {name: '', message: serializeThrownValue(error).toLowerCase()}; } export {StorageErrorClass, getErrorParts}; diff --git a/tests/unit/storage/getErrorPartsTest.ts b/tests/unit/storage/getErrorPartsTest.ts new file mode 100644 index 000000000..54fb7a64f --- /dev/null +++ b/tests/unit/storage/getErrorPartsTest.ts @@ -0,0 +1,40 @@ +import {getErrorParts} from '../../../lib/storage/errors'; + +describe('getErrorParts', () => { + it('should extract name and message from Error instances', () => { + expect(getErrorParts(new TypeError('Could not be cloned'))).toEqual({ + name: 'typeerror', + message: 'could not be cloned', + }); + }); + + it('should lowercase string throws', () => { + expect(getErrorParts('Quota exceeded')).toEqual({name: '', message: 'quota exceeded'}); + }); + + it('should treat null and undefined as empty', () => { + expect(getErrorParts(null)).toEqual({name: '', message: ''}); + expect(getErrorParts(undefined)).toEqual({name: '', message: ''}); + }); + + it('should serialize plain objects so classifiers can match fields', () => { + expect(getErrorParts({message: 'QuotaExceededError'})).toEqual({ + name: '', + message: '{"message":"quotaexceedederror"}', + }); + }); + + it('should not throw on circular objects', () => { + const circular: {self?: unknown} = {}; + circular.self = circular; + + expect(() => getErrorParts(circular)).not.toThrow(); + expect(getErrorParts(circular)).toEqual({name: '', message: '[object object]'}); + }); + + it('should not throw on BigInt', () => { + const value = BigInt(1); + expect(() => getErrorParts(value)).not.toThrow(); + expect(getErrorParts(value)).toEqual({name: '', message: '1'}); + }); +});