Closed Bug 1811782 Opened 2 years ago Closed 1 year ago

`document.write()` does not trigger `MutationObserver` of `document.implementation.createHTMLDocument()`

Categories

(Core :: DOM: Core & HTML, defect)

Firefox 109
defect

Tracking

()

RESOLVED WONTFIX

People

(Reporter: dgp1130422, Unassigned)

Details

User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36

Steps to reproduce:

Create a document using document.implementation.createHTMLDocument() and then add a MutationObserver to it. Then use .write() on the document, and the MutationObserver does not trigger from the change.

I've put together a StackBlitz reproducing the issue: https://stackblitz.com/edit/typescript-cxeurn?file=index.ts

Full source code which reproduces the bug:

const doc = document.implementation.createHTMLDocument();

const obs = new MutationObserver((records) => {
  // BUG: This is never called.
  for (const record of records) {
    for (const added of record.addedNodes) {
      console.log('Muted inner document.');
      document.body.append(added);
    }
  }
});
obs.observe(doc.body, { childList: true });

doc.write('<h2>Hello, World!</h2>');
console.log('Written to inner document!');

I came across this bug while experimenting with streamable HTML fragments and the demo basically doesn't work in Firefox due to this bug:
https://blog.dwac.dev/posts/streamable-html-fragments/
https://github.com/dgp1130/html-fragments-demo/tree/blog-post-streaming/

Actual results:

The MutationObserver callback is never invoked.

Expected results:

The MutationObserver callback should be invoked when the written HTML mutates the inner document.

While I'm not familiar with the spec, I believe triggering the MutationObserver is the correct behavior and Chrome does this as I would expect. Otherwise it is possible to mutate the document without triggering the MutationObserver, which seems wrong to me.

The Bugbug bot thinks this bug should belong to the 'Core::DOM: Core & HTML' component, and is moving the bug to that component. Please correct in case you think the bot is wrong.

Component: Untriaged → DOM: Core & HTML
Product: Firefox → Core

document.write changes the body element in Firefox and you're observing on doc.body.
If you observe changes on doc itself, then you get the mutation records.

I believe the spec goes from
https://html.spec.whatwg.org/#document-write-steps to
https://html.spec.whatwg.org/#document-open-steps
where step 11 clears all the child nodes of the document.

Ah thanks for pointing that out. It took me a bit to really understand your comment and why this is happening, so to summarize for my own benefit and any others who find this:

  1. Page observes doc.body.
  2. Page calls doc.write().
  3. doc.write() implicitly calls doc.open() (step 4.2 in https://html.spec.whatwg.org/#document.write())
  4. doc.open() resets the page to an empty state (step 11 in https://html.spec.whatwg.org/#document-open-steps)
  5. doc.body is now null (you can observe this with doc.open(); console.log(doc.body);
  6. doc.write() writes the input string <h2>Hello, World!</h2> to the document, creating a new <body> tag in doc.body.
  7. The mutation triggers on the current doc.body (the implicitly created <body> tag), not the doc.body which is being observed.

So the solution from a JS perspective is to open the document prior to observing it. Changing the timeline to:

  1. Page calls doc.open().
  2. doc.open() resets the document and doc.body is now null.
  3. Call doc.write('<!DOCTYPE html><html><head></head><body>'); to write enough of the HTML to create a <body /> tag.
  4. Observe doc.body.
  5. doc.write() the rest of the page.
  6. Mutations trigger the observer as expected.

This seems to work for both Chrome and Firefox: https://stackblitz.com/edit/typescript-5yxgzg?file=index.ts

I also tried to observe doc directly without opening it first and this does work in Firefox, but doesn't trigger any events in Chrome. Using doc.body works in Chrome but doesn't trigger any events in Firefox, so it seems like there's some disagreement about where events should be observed. I feel like Chrome's behavior is right since I am not including subtree: true, so I would expect observing a document to trigger mutations to the documentElement or the <html /> tag (if there's a difference between those things), not the <body /> or <head /> elements. I'm not entirely sure how the root document acts in a node hierarchy like this, so I'll defer to others as to which is the correct behavior, but it's definitely an inconsistency which would be good to resolve.

Resetting the document on doc.write() seems a bit redundant for the case of document.implementation.createHTMLDocument() which is already empty, but I guess it has a <body /> tag, so maybe not completely empty for purposes of parsing input HTML? My solution just hard-codes enough of the HTML to create a <body /> tag, which is good enough for my use case because the HTML I'm writing won't (and shouldn't) include that. Other use cases would probably need to write the input and then poll the doc.body attribute to see if it has been created, handle any elements already present, and then set up the MutationObserver. That sounds a bit annoying, but probably feasible at least.

I think there's also a potential bug in Chrome and Safari here that "Observing doc.body shouldn't observe mutations from doc.write() on a closed document". Step 11 of document.open() steps resets the whole document, so I would expect that to reset <head /> and <body /> together. As I see it, Firefox's behavior is correct with respect to observing doc.body, so that might be a bug for other browsers to align with Firefox in that respect at least.

The spec used to have a comment: <!-- as of 2009-03-30, only WebKit fired mutation events here. -->. This seems to be one of those things where of the set Trident, Gecko, WebKit, only WebKit did something and Blink inherited whatever that was so now of the set Gecko, WebKit, Blink, only Gecko doesn't do it.

Severity: -- → S3

I confused MutationObserver and mutation events above.

I'm interested in understanding why the test case in Chrome shows node additions to the body, when the node that's reported to be added should be added to the new body element and not the old body element that's being observed.

Severity: S3 → --

Needinfoing smaug for a further look.

Flags: needinfo?(smaug)

Looks like I've been slow with this. hsivonen, you might still want to take a look? But as far as I see, Firefox behaves correctly here.

Flags: needinfo?(smaug) → needinfo?(hsivonen)

WONTFIX per comment 8.

Status: UNCONFIRMED → RESOLVED
Closed: 1 year ago
Flags: needinfo?(hsivonen)
Resolution: --- → WONTFIX
You need to log in before you can comment on or make changes to this bug.