Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions lib/storage/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve messages from cross-realm errors

When an Error or DOMException originates in another realm, such as an iframe, both instanceof checks are false and its name and message properties are non-enumerable, so this returns {}. The previous String(error) path retained text such as Error: QuotaExceededError; losing it now causes classifyIDBError to return UNKNOWN instead of CAPACITY (and similarly hides other recognizable messages). Read error-like name/message fields structurally or fall back to String when JSON serialization erases them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-realm instanceof is a real JS thing, but it does not apply here.

This classifier only sees throws from our own IndexedDB / SQLite providers in the same window. Those are same-realm Error / DOMException, so the existing instanceof checks still match. We do not load Onyx inside an iframe that would produce a foreign-realm error.

Even in the hypothetical miss, this is not a regression vs main: main already used String(error) only after instanceof failed. JSON.stringify of an Error is {}, so we would lose name/message either way. classifyIDBError matching CAPACITY from QuotaExceededError still goes through the instanceof path.

Not changing this.

} 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};
40 changes: 40 additions & 0 deletions tests/unit/storage/getErrorPartsTest.ts
Original file line number Diff line number Diff line change
@@ -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'});
});
});
Loading