diff --git a/.changeset/dark-rooms-tease.md b/.changeset/dark-rooms-tease.md new file mode 100644 index 0000000000..6dff0861bb --- /dev/null +++ b/.changeset/dark-rooms-tease.md @@ -0,0 +1,5 @@ +--- +"rrweb-snapshot": patch +--- + +Omit the `srcdoc` attribute when rebuilding iframe elements, so the browser doesn't race its own async document load against rrweb's own reconstruction of the iframe's contents (which could desync the mirror and throw `insertBefore` errors) diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index a6ef85f01b..d0a4ccfdec 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -426,6 +426,19 @@ function buildNode( 'rrweb-original-srcset', n.attributes.srcset as string, ); + } else if (tagName === 'iframe' && name === 'srcdoc') { + /** + * Setting `srcdoc` makes the browser asynchronously parse and load + * its own document into the iframe, racing against (and getting + * clobbered by or clobbering) the document we reconstruct for this + * iframe from its separately-recorded child nodes/mutations. That + * race can leave the mirror out of sync with the live DOM, causing + * later mutations to target nodes that no longer exist (e.g. + * insertBefore throwing "parameter 1 is not of type 'Node'"). + * Omit it; our own reconstruction is the source of truth. + * @see https://github.com/rrweb-io/rrweb/issues/1736 + */ + // ignore } else { node.setAttribute(name, value.toString()); } diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 27571c164f..129acd4b68 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -411,6 +411,38 @@ describe('rebuild', function () { }); }); + describe('iframe srcdoc', function () { + it('omits the srcdoc attribute so the browser does not load its own document into the iframe', function () { + /** + * Setting `srcdoc` on a live iframe makes the browser asynchronously + * parse and load that markup into the iframe's contentDocument, racing + * against rrweb's own reconstruction of the iframe's document (built + * separately from recorded child nodes/mutations). That race can + * desync the mirror from the live DOM, later causing mutations to + * target nodes that no longer exist. + * @see https://github.com/rrweb-io/rrweb/issues/1736 + */ + const node = buildNodeWithSN( + { + id: 1, + tagName: 'iframe', + type: NodeType.Element, + attributes: { + srcdoc: 'hi', + }, + childNodes: [], + }, + { + doc: document, + mirror, + hackCss: false, + cache, + }, + ) as HTMLIFrameElement; + expect(node.hasAttribute('srcdoc')).toBe(false); + }); + }); + describe('rr_width/rr_height', function () { it('rebuild blocked element with correct dimensions', function () { const node = buildNodeWithSN(