LibWeb: Parse srcdoc documents synchronously during activation

Bypass the async body-reading pipeline for about:srcdoc iframes whose
body bytes are already in memory. Set up a deferred parser at document
load time and run the post-activation update synchronously, so the body
element exists before parent script can observe the new document via
contentDocument. This matches Chrome and Firefox behavior for srcdoc
iframes and fixes the flaky test
`set-innerHTML-inside-iframe-srcdoc-document.html` that relied on body
being non-null.

Co-authored-by: Tim Ledbetter <tim.ledbetter@ladybird.org>
This commit is contained in:
Jelle Raaijmakers 2026-04-16 00:26:13 +02:00 committed by Tim Ledbetter
parent 732063aca9
commit a5e1c33743
3 changed files with 32 additions and 0 deletions

View file

@ -813,6 +813,7 @@ public:
bool ready_to_run_scripts() const { return m_ready_to_run_scripts; }
void set_ready_to_run_scripts();
void set_deferred_parser_start(GC::Ref<GC::Function<void()>>);
bool has_deferred_parser_start() const { return m_deferred_parser_start; }
void set_latest_entry(RefPtr<HTML::SessionHistoryEntry>);

View file

@ -80,6 +80,28 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_html_document(HTML::Navi
HTML::HTMLParser::the_end(document);
}
// AD-HOC: For about:srcdoc, the body bytes are always immediately available in the response source (the srcdoc
// string was inlined when the navigation params were created). Bypass the async body-reading pipeline
// and set up a deferred parser directly. Combined with running the post-activation update synchronously
// when a deferred parser is set, this guarantees the body element exists before the document becomes
// observable to the parent — matching Chrome and Firefox behavior for srcdoc iframes.
//
// FIXME: This only fixes the transient `contentDocument.body === null` race for srcdoc. The same race exists in
// Ladybird for any other same-origin async iframe load (notably blob: URLs and same-origin HTTP), where
// Chrome and Firefox also keep `contentDocument` pointed at the initial about:blank until the new document
// reaches readyState="interactive". A spec-aligned fix would split "what the parent sees via
// contentDocument" from the navigable's active document, swapping the parent-visible pointer only at
// parser readiness.
else if (auto const* data = navigation_params.response->body()->source().get_pointer<ByteBuffer>();
data && document->url() == URL::about_srcdoc()) {
auto mime_type = Fetch::Infrastructure::extract_mime_type(navigation_params.response->header_list());
auto url = navigation_params.response->url().value();
auto parser = HTML::HTMLParser::create_with_uncertain_encoding(document, *data, mime_type);
document->set_deferred_parser_start(GC::create_function(document->heap(), [parser, url] {
parser->run(url);
}));
}
// 3. Otherwise, create an HTML parser and associate it with the document.
// Each task that the networking task source places on the task queue while fetching runs must then fill the
// parser's input byte stream with the fetched bytes and cause the HTML parser to perform the appropriate

View file

@ -942,6 +942,15 @@ void ApplyHistoryStepState::process_continuations()
if (target_entry->document_state()->document_id() == displayed_document_id) {
update_document();
}
// AD-HOC: When the document already has its parser pre-loaded with in-memory data (currently set up
// only for about:srcdoc), perform updateDocument synchronously instead of queueing it.
// updateDocument calls Document::set_ready_to_run_scripts(), which kicks off the deferred
// parser. Running it in the same task as activation guarantees the body element exists before
// script in the parent navigable can observe the new document — matching Chrome and Firefox
// behavior for srcdoc iframes.
else if (resolved_document->has_deferred_parser_start()) {
update_document();
}
// 5. Otherwise, queue a global task on the navigation and traversal task source given targetEntry's document's relevant global object to perform updateDocument
else {
queue_global_task(Task::Source::NavigationAndTraversal, relevant_global_object(*resolved_document), GC::create_function(heap(), move(update_document)));