LibWeb+LibWebView: Preserve crashed page URLs on crash

Load the browser-generated crash page as a synthetic response for the
URL that was active when WebContent exited. This keeps the session
history entry, response URL, and created Document aligned with the same
navigation URL, so reload targets the original page without creating a
local document for an HTTP(S) history entry.

Suppress history metadata updates from the generated page and declare
an inert rel=icon. Fallback favicon loading now follows the HTML
condition that no link with the icon keyword exists, which avoids the
credentialed /favicon.ico request from the crashed origin.
This commit is contained in:
Andreas Kling 2026-05-20 16:42:08 +02:00 committed by Andreas Kling
parent 31a9878223
commit 8eb74bf747
12 changed files with 183 additions and 20 deletions

View file

@ -21,6 +21,7 @@
#include <LibWeb/DOM/DOMTokenList.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/HTMLCollection.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/Fetch/Fetching/Fetching.h>
@ -151,6 +152,11 @@ bool HTMLLinkElement::has_loaded_icon() const
return m_relationship & Relationship::Icon && m_loaded_icon.has_value();
}
bool HTMLLinkElement::has_icon_keyword() const
{
return m_relationship & Relationship::Icon;
}
void HTMLLinkElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_)
{
Base::attribute_changed(name, old_value, value, namespace_);
@ -993,6 +999,14 @@ void HTMLLinkElement::load_fallback_favicon_if_needed(GC::Ref<DOM::Document> doc
return;
if (!document->url().scheme().is_one_of("http"sv, "https"sv))
return;
auto icon_link_elements = DOM::HTMLCollection::create(*document, DOM::HTMLCollection::Scope::Descendants, [](DOM::Element const& element) {
if (!is<HTMLLinkElement>(element))
return false;
return static_cast<HTMLLinkElement const&>(element).has_icon_keyword();
});
if (icon_link_elements->length() != 0)
return;
// AD-HOC: Don't load fallback favicon for auxiliary browsing contexts (popup windows).
// This matches the behavior observed in Chrome and Firefox, and avoids unnecessary network requests

View file

@ -38,6 +38,7 @@ public:
GC::Ref<DOM::DOMTokenList> sizes();
bool has_loaded_icon() const;
bool has_icon_keyword() const;
bool load_favicon_and_use_if_window_is_active();
static void load_fallback_favicon_if_needed(GC::Ref<DOM::Document>);

View file

@ -2165,8 +2165,81 @@ void Navigable::begin_navigation(NavigateParams params)
// 7. Let navigationParams be null.
NavigationParamsVariant navigation_params = Navigable::NullOrError {};
// FIXME: 8. If response is non-null:
// 8. If response is non-null:
if (response) {
auto response_url = response->url();
VERIFY(response_url.has_value());
// 1. Let sourcePolicyContainer be a clone of the sourceDocument's policy container, if
// sourceDocument is not null; otherwise null.
auto source_policy_container = source_snapshot_params->source_policy_container;
// 2. Let policyContainer be the result of determining navigation params policy container given
// response's URL, null, sourcePolicyContainer, navigable's container document's policy container,
// and null.
GC::Ptr<PolicyContainer> parent_policy_container;
if (auto container_document = this->container_document())
parent_policy_container = container_document->policy_container();
else if (*response_url == URL::about_srcdoc()) {
// NOTE: Specification assumes that only navigables corresponding to iframes can be navigated to about:srcdoc.
// We also use srcdoc to implement load_html() for top level navigables so we need a policy container
// because the navigable might not have a container.
parent_policy_container = heap().allocate<PolicyContainer>(heap());
}
auto policy_container = determine_navigation_params_policy_container(*response_url, heap(), {}, source_policy_container, parent_policy_container, {});
// 3. Let finalSandboxFlags be the union of targetSnapshotParams's sandboxing flags and
// policyContainer's CSP list's CSP-derived sandboxing flags.
auto final_sandbox_flags = target_snapshot_params.sandboxing_flags | policy_container->csp_list->csp_derived_sandboxing_flags();
// 4. Let responseOrigin be the result of determining the origin given response's URL,
// finalSandboxFlags, and documentState's initiator origin.
auto response_origin = determine_the_origin(response_url, final_sandbox_flags, document_state->initiator_origin());
// 5. Let coop be a new opener policy.
OpenerPolicy response_coop = {};
// 6. Let coopEnforcementResult be a new opener policy enforcement result with
// url: response's URL
// origin: responseOrigin
// opener policy: coop
OpenerPolicyEnforcementResult coop_enforcement_result {
.url = *response_url,
.origin = response_origin,
.opener_policy = response_coop,
};
// 7. Set navigationParams to a new navigation params, with
// id: navigationId
// navigable: navigable
// request: null
// response: response
// fetch controller: null
// commit early hints: null
// COOP enforcement result: coopEnforcementResult
// reserved environment: null
// origin: responseOrigin
// policy container: policyContainer
// final sandboxing flag set: finalSandboxFlags
// opener policy: coop
// FIXME: navigation timing type: "navigate"
// about base URL: documentState's about base URL
// user involvement: userInvolvement
navigation_params = heap().allocate<NavigationParams>(
navigation_id,
this,
nullptr,
response,
nullptr,
nullptr,
move(coop_enforcement_result),
nullptr,
move(response_origin),
policy_container,
final_sandbox_flags,
response_coop,
document_state->about_base_url(),
user_involvement);
}
// 9. Attempt to populate the history entry's document for historyEntry, given navigable, "navigate",

View file

@ -16,6 +16,8 @@
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Bodies.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/HTMLIFrameElement.h>
@ -128,6 +130,31 @@ void Page::load_html(StringView html)
.user_involvement = HTML::UserNavigationInvolvement::BrowserUI });
}
void Page::load_html(StringView html, URL::URL const& url)
{
// FIXME: #23909 Figure out why GC threshold does not stay low when repeatedly loading html from the WebView
heap().collect_garbage();
auto document = top_level_traversable()->active_document();
auto& realm = document->realm();
auto html_string = String::from_utf8(html).release_value_but_fixme_should_propagate_errors();
auto response = Fetch::Infrastructure::Response::create(realm.vm());
response->url_list().append(url);
response->header_list()->append({ "Content-Type"sv, "text/html"sv });
response->set_body(Fetch::Infrastructure::byte_sequence_as_body(realm, html_string.bytes()));
HTML::Navigable::NavigateParams params { .url = url,
.source_document = *document,
.response = response,
.user_involvement = HTML::UserNavigationInvolvement::BrowserUI };
if (url == URL::about_srcdoc())
params.document_resource = move(html_string);
(void)top_level_traversable()->navigate(move(params));
}
void Page::reload()
{
top_level_traversable()->reload();

View file

@ -97,6 +97,7 @@ public:
void load(URL::URL const&);
void load_html(StringView);
void load_html(StringView, URL::URL const&);
void reload();

View file

@ -18,6 +18,7 @@ constexpr inline auto ERROR_HTML_HEADER = R"~~~(
<head>
<meta charset="UTF-8" />
<title>Error!</title>
{}
<style>
:root {{
color-scheme: light dark;
@ -67,6 +68,12 @@ constexpr inline auto ERROR_HTML_HEADER = R"~~~(
</header>
)~~~"sv;
// Declaring an icon prevents the spec's fallback /favicon.ico fetch. The empty
// data URL is local and intentionally does not decode as an icon.
constexpr inline auto NO_FALLBACK_FAVICON_LINK = R"~~~(
<link rel="icon" href="data:," />
)~~~"sv;
constexpr inline auto ERROR_HTML_FOOTER = R"~~~(
</body>
</html>

View file

@ -120,7 +120,8 @@ void ViewImplementation::set_favicon(Badge<WebContentClient>, Gfx::Bitmap const&
if (m_favicon_base64_png.has_value()) {
Application::bookmark_store().update_favicon(m_url, *m_favicon_base64_png);
Application::history_store().update_favicon(m_url, *m_favicon_base64_png);
if (!m_should_suppress_history_for_current_load)
Application::history_store().update_favicon(m_url, *m_favicon_base64_png);
}
if (on_favicon_change)
@ -189,21 +190,33 @@ void ViewImplementation::set_system_visibility_state(Web::HTML::VisibilityState
void ViewImplementation::load(URL::URL const& url)
{
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
set_url(url);
client().async_load_url(page_id(), url);
}
void ViewImplementation::load_html(StringView html)
{
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
client().async_load_html(page_id(), html);
}
void ViewImplementation::load_crash_page_html(StringView html, URL::URL const& crashed_url)
{
m_should_suppress_history_for_current_load = true;
m_should_suppress_history_for_next_load = true;
set_url(crashed_url);
client().async_load_html_with_url(page_id(), html, crashed_url);
}
void ViewImplementation::load_navigation_error_page(StringView text)
{
auto message = MUST(String::formatted("Failed to load \"{}\"", text));
StringBuilder builder;
builder.appendff(ERROR_HTML_HEADER, ERROR_SVG, message);
builder.appendff(ERROR_HTML_HEADER, ""sv, ERROR_SVG, message);
builder.append("<p>If you were trying to enter a search query, please enable search in <a href=\"about:settings#search\">settings</a>.</p>"sv);
builder.append(ERROR_HTML_FOOTER);
load_html(builder.string_view());
@ -211,11 +224,15 @@ void ViewImplementation::load_navigation_error_page(StringView text)
void ViewImplementation::reload()
{
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
client().async_reload(page_id());
}
void ViewImplementation::traverse_the_history_by_delta(int delta)
{
m_should_suppress_history_for_current_load = false;
m_should_suppress_history_for_next_load = false;
client().async_traverse_the_history_by_delta(page_id(), delta);
}
@ -774,10 +791,10 @@ void ViewImplementation::handle_web_content_process_crash(LoadErrorPage load_err
auto escaped_url = escape_html_entities(m_url.serialize());
StringBuilder builder;
builder.appendff(ERROR_HTML_HEADER, CRASH_ERROR_SVG, "Ladybird flew off-course!"sv);
builder.appendff(ERROR_HTML_HEADER, NO_FALLBACK_FAVICON_LINK, CRASH_ERROR_SVG, "Ladybird flew off-course!"sv);
builder.appendff("<p>The web page <a href=\"{}\">{}</a> has crashed.<br><br>You can reload the page to try again.</p>", escaped_url, escaped_url);
builder.append(ERROR_HTML_FOOTER);
load_html(builder.string_view());
load_crash_page_html(builder.string_view(), m_url);
}
}

View file

@ -297,6 +297,7 @@ protected:
void apply_zoom_for_current_host();
void handle_resize();
void load_crash_page_html(StringView, URL::URL const& crashed_url);
enum class CreateNewClient {
No,
@ -392,6 +393,8 @@ protected:
OwnPtr<Gfx::SharedImageBuffer> m_backup_shared_image_buffer;
Web::DevicePixelSize m_backup_bitmap_size;
bool m_should_suppress_history_for_current_load { false };
bool m_should_suppress_history_for_next_load { false };
size_t m_crash_count = 0;
RefPtr<Core::Timer> m_repeated_crash_timer;

View file

@ -253,6 +253,9 @@ void WebContentClient::did_start_loading(u64 page_id, URL::URL url, bool is_redi
m_history_recorded_urls_for_current_load.remove(page_id);
if (auto view = view_for_page_id(page_id); view.has_value()) {
view->m_should_suppress_history_for_current_load = view->m_should_suppress_history_for_next_load;
view->m_should_suppress_history_for_next_load = false;
view->set_url({}, url);
if (view->on_load_start)
@ -275,26 +278,34 @@ void WebContentClient::did_finish_loading(u64 page_id, URL::URL url)
}
if (auto view = view_for_page_id(page_id); view.has_value()) {
view->set_url({}, url);
auto client_url = url;
// Browser-generated pages can finish with an internal document URL.
// Keep exposing the URL accepted at load start for suppressed loads.
if (view->m_should_suppress_history_for_current_load)
client_url = view->url();
else
view->set_url({}, url);
auto should_update_history = !view->m_should_suppress_history_for_current_load;
auto title = history_title(view->title(), url);
dbgln_if(WEBVIEW_HISTORY_DEBUG, "[History] Load finished for page {} at '{}' with title '{}'",
page_id,
url,
title.has_value() ? title->bytes_as_string_view() : "<none>"sv);
maybe_record_history_visit_for_current_load(page_id, url, title, "load finish"sv);
if (title.has_value())
Application::history_store().update_title(url, *title);
if (view->favicon_base64_png().has_value())
Application::history_store().update_favicon(url, *view->favicon_base64_png());
if (should_update_history) {
dbgln_if(WEBVIEW_HISTORY_DEBUG, "[History] Load finished for page {} at '{}' with title '{}'",
page_id,
url,
title.has_value() ? title->bytes_as_string_view() : "<none>"sv);
maybe_record_history_visit_for_current_load(page_id, url, title, "load finish"sv);
if (title.has_value())
Application::history_store().update_title(url, *title);
if (view->favicon_base64_png().has_value())
Application::history_store().update_favicon(url, *view->favicon_base64_png());
}
if (view->on_load_finish)
view->on_load_finish(url);
view->on_load_finish(client_url);
for (auto const& [id, listener] : view->m_navigation_listeners) {
if (listener.on_load_finish)
listener.on_load_finish(url);
listener.on_load_finish(client_url);
}
}
}
@ -365,7 +376,7 @@ void WebContentClient::did_change_title(u64 page_id, Utf16String title)
process->set_title(title);
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (!title.is_empty()) {
if (!title.is_empty() && !view->m_should_suppress_history_for_current_load) {
auto title_utf8 = title.to_utf8();
maybe_record_history_visit_for_current_load(page_id, view->url(), title_utf8, "title change"sv);
@ -702,7 +713,8 @@ void WebContentClient::did_change_favicon(u64 page_id, Gfx::ShareableBitmap favi
}
if (auto view = view_for_page_id(page_id); view.has_value()) {
maybe_record_history_visit_for_current_load(page_id, view->url(), history_title(view->title(), view->url()), "favicon change"sv);
if (!view->m_should_suppress_history_for_current_load)
maybe_record_history_visit_for_current_load(page_id, view->url(), history_title(view->title(), view->url()), "favicon change"sv);
view->set_favicon({}, *favicon.bitmap());
}
}

View file

@ -276,6 +276,12 @@ void ConnectionFromClient::load_html(u64 page_id, ByteString html)
page->page().load_html(html);
}
void ConnectionFromClient::load_html_with_url(u64 page_id, ByteString html, URL::URL url)
{
if (auto page = this->page(page_id); page.has_value())
page->page().load_html(html, url);
}
void ConnectionFromClient::reload(u64 page_id)
{
if (auto page = this->page(page_id); page.has_value())

View file

@ -71,6 +71,7 @@ private:
virtual void update_screen_rects(u64 page_id, Vector<Web::DevicePixelRect>, u32) override;
virtual void load_url(u64 page_id, URL::URL) override;
virtual void load_html(u64 page_id, ByteString) override;
virtual void load_html_with_url(u64 page_id, ByteString, URL::URL) override;
virtual void reload(u64 page_id) override;
virtual void traverse_the_history_by_delta(u64 page_id, i32 delta) override;
virtual void set_viewport(u64 page_id, Web::DevicePixelSize, double device_pixel_ratio, Web::ViewportIsFullscreen is_fullscreen) override;

View file

@ -41,6 +41,7 @@ endpoint WebContentServer
load_url(u64 page_id, URL::URL url) =|
load_html(u64 page_id, ByteString html) =|
load_html_with_url(u64 page_id, ByteString html, URL::URL url) =|
reload(u64 page_id) =|
traverse_the_history_by_delta(u64 page_id, i32 delta) =|