From 43418f8334f57a6c7894e5412a2dc64f37f037a0 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Mon, 5 May 2025 16:19:13 +0000 Subject: [PATCH] fix: avoid browser crashes when serializing browser logs of complex objects If we are seeing too many object iterations, we know something is off. This can happen when we are seeing e.g. large linked lists where every element recursively has access to the full list again. This will not quickly yield circular early bail-outs, but instead result in the stringification to take LOTS of processing time and results in browser crashes. We should limit object iterations for this reason, and instead expect people to inspect such logs in the browser directly. Related #2798. --- .changeset/three-gifts-clap.md | 14 +++++++ packages/browser-logs/src/serialize.ts | 12 ++++++ .../test/serialize-deserialize.test.ts | 40 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 .changeset/three-gifts-clap.md diff --git a/.changeset/three-gifts-clap.md b/.changeset/three-gifts-clap.md new file mode 100644 index 000000000..81da64653 --- /dev/null +++ b/.changeset/three-gifts-clap.md @@ -0,0 +1,14 @@ +--- +'@web/browser-logs': patch +--- + +Avoid browser crashes when serializing browser logs of complex objects + +If we are seeing too many object iterations, we know something is off. This can +happen when we are seeing e.g. large linked lists where every element recursively +has access to the full list again. This will not quickly yield circular early bail-outs, +but instead result in the stringification to take LOTS of processing time and results +in browser crashes. We should limit object iterations for this reason, and instead expect +people to inspect such logs in the browser directly. + +Related #2798. diff --git a/packages/browser-logs/src/serialize.ts b/packages/browser-logs/src/serialize.ts index 0a458468e..4b3cdc34d 100644 --- a/packages/browser-logs/src/serialize.ts +++ b/packages/browser-logs/src/serialize.ts @@ -71,6 +71,7 @@ function serializeObject(value: any) { function createReplacer() { // maintain a stack of seen objects to handle circular references var objectStack: any[] = []; + var objectIterations = 0; return function replacer(this: any, key: string, value: unknown) { if (this[KEY_WTR_TYPE]) { @@ -113,6 +114,17 @@ function createReplacer() { } objectStack.unshift(value); + // If we are seeing too many object iterations, we know something is off. This can + // happen when we are seeing e.g. large linked lists where every element recursively + // has access to the full list again. This will not quickly yield circular early bail-outs, + // but instead result in the stringification to take LOTS of processing time and results + // in browser crashes. We should limit object iterations for this reason, and instead expect + // people to inspect such logs in the browser directly. + objectIterations++; + if (objectIterations > 10_000) { + return '[Serialization Limit]'; + } + if (Array.isArray(value)) { return value; } diff --git a/packages/browser-logs/test/serialize-deserialize.test.ts b/packages/browser-logs/test/serialize-deserialize.test.ts index 5a3232684..eb949d50a 100644 --- a/packages/browser-logs/test/serialize-deserialize.test.ts +++ b/packages/browser-logs/test/serialize-deserialize.test.ts @@ -336,6 +336,46 @@ describe('serialize deserialize', function () { expect(deserialized.x).to.equal('[Circular]'); }); + it('handles complex circular references', async () => { + const serialized = await page.evaluate(() => { + interface ChipNode { + idx: number; + next?: ChipNode; + prev?: ChipNode; + val: unknown; + } + class MatChip { + viewRef: { nodes: ChipNode[] } = { nodes: [] }; + + constructor() { + let prevNode: ChipNode | null = null; + for (let i = 0; i < 1000; i++) { + const newNode: ChipNode = { + idx: i, + val: this, + next: this.viewRef.nodes[0], + }; + + this.viewRef.nodes.push(newNode); + + if (prevNode) { + prevNode.next = newNode; + newNode.prev = prevNode; + } + + prevNode = newNode; + } + } + } + return (window as any)._serialize(new MatChip()); + }); + + await deserialize(serialized); + + // No assertion. Simply expect this to pass and not crash. + expect(true).to.eq(true); + }); + it('handles errors', async () => { const serialized = await page.evaluate(() => { const c = () => new Error('my error msg');