ignore late page/title events from suspended tabs (white screen on tab restore)

when a loading tab was suspended its webview was destroyed while events
were still queued, and a finished load from the dying webview could
overwrite the tab's stored URL with an intermediate redirect URL. on
switch-back the tab then reloaded the wrong page and stayed blank.

each webview instance now gets a generation (epoch): events carry it and
are dropped when they don't match, and suspending a tab invalidates its
epoch. verified tab cycling still restores pages normally.
This commit is contained in:
Martin Dvořák 2026-10-11 19:35:51 +02:00
commit 0c6b62ae3b

View file

@ -39,10 +39,12 @@ enum UserEvent {
ChromeIpc(String),
TitleChanged {
tab_id: u32,
epoch: u32,
title: String,
},
PageLoaded {
tab_id: u32,
epoch: u32,
url: String,
},
DownloadStarted {
@ -73,6 +75,10 @@ struct Tab {
url: String,
kir_enabled: bool,
webview: Option<WebView>,
// Identifies the current webview instance for this tab. Events (page load,
// title) carry the epoch they were emitted from so that late events from a
// destroyed webview (suspended tab) cannot corrupt the tab state.
epoch: u32,
back_stack: Vec<String>,
forward_stack: Vec<String>,
pending_nav_from_stack: bool,
@ -94,6 +100,7 @@ struct BrowserState {
downloads_panel_open: bool,
active: usize,
next_id: u32,
next_epoch: u32,
private: bool,
user_agent: Option<String>,
web_context: Option<WebContext>,
@ -133,6 +140,13 @@ impl BrowserState {
self.next_id += 1;
id
}
fn alloc_epoch(&mut self) -> u32 {
self.next_epoch = self.next_epoch.wrapping_add(1);
if self.next_epoch == 0 {
self.next_epoch = 1;
}
self.next_epoch
}
fn find_index(&self, tab_id: u32) -> Option<usize> {
self.tabs.iter().position(|t| t.id == tab_id)
}
@ -1182,6 +1196,7 @@ pub fn launch_webkit(args: &Cli) -> Result<()> {
downloads_panel_open: false,
active: 0,
next_id: 1,
next_epoch: 1,
private: args.private,
user_agent: if args.ua_chrome {
Some(chrome_user_agent())
@ -1385,16 +1400,23 @@ fn handle_event(
) -> Result<()> {
match ev {
UserEvent::ChromeIpc(msg) => handle_chrome_ipc(&msg, window, proxy, state)?,
UserEvent::TitleChanged { tab_id, title } => {
UserEvent::TitleChanged {
tab_id,
epoch,
title,
} => {
if let Some(i) = state.find_index(tab_id) {
if !title.trim().is_empty() {
if state.tabs[i].epoch == epoch && !title.trim().is_empty() {
state.tabs[i].title = title;
push_tabs(state);
}
}
}
UserEvent::PageLoaded { tab_id, url } => {
UserEvent::PageLoaded { tab_id, epoch, url } => {
if let Some(i) = state.find_index(tab_id) {
if state.tabs[i].epoch != epoch {
return Ok(());
}
if !url.is_empty() && url != "about:blank" {
let previous = state.tabs[i].url.clone();
if url != previous {
@ -1701,6 +1723,7 @@ fn handle_chrome_ipc(
state.tabs[i].kir_enabled = !state.tabs[i].kir_enabled;
if i != state.active && !state.tabs[i].kir_enabled {
state.tabs[i].webview = None;
state.tabs[i].epoch = 0;
}
apply_visibility(state);
push_tabs(state);
@ -1883,6 +1906,7 @@ fn build_tab_webview(
user_agent: Option<&str>,
mut web_context: Option<&mut WebContext>,
tab_id: u32,
epoch: u32,
url: &str,
bounds: Rect,
) -> Result<(WebView, WebViewHostMode)> {
@ -1902,6 +1926,7 @@ fn build_tab_webview(
user_agent,
&init_script,
tab_id,
epoch,
url,
bounds,
)
@ -1919,6 +1944,7 @@ fn build_tab_webview(
user_agent,
&init_script,
tab_id,
epoch,
url,
bounds,
)
@ -1935,6 +1961,7 @@ fn build_tab_webview(
user_agent,
&init_script,
tab_id,
epoch,
url,
bounds,
)
@ -1957,6 +1984,7 @@ fn build_tab_webview(
user_agent,
&init_script,
tab_id,
epoch,
url,
bounds,
)
@ -1982,6 +2010,7 @@ fn build_tab_webview(
user_agent,
&init_script,
tab_id,
epoch,
url,
bounds,
)
@ -2009,6 +2038,7 @@ fn tab_webview_builder<'a>(
user_agent: Option<&str>,
init_script: &str,
tab_id: u32,
epoch: u32,
url: &str,
bounds: Rect,
) -> WebViewBuilder<'a> {
@ -2036,11 +2066,15 @@ fn tab_webview_builder<'a>(
false
})
.with_document_title_changed_handler(move |title| {
let _ = title_proxy.send_event(UserEvent::TitleChanged { tab_id, title });
let _ = title_proxy.send_event(UserEvent::TitleChanged {
tab_id,
epoch,
title,
});
})
.with_on_page_load_handler(move |event, url| {
if matches!(event, PageLoadEvent::Finished) {
let _ = page_proxy.send_event(UserEvent::PageLoaded { tab_id, url });
let _ = page_proxy.send_event(UserEvent::PageLoaded { tab_id, epoch, url });
}
})
.with_download_started_handler(move |url, destination| {
@ -2126,6 +2160,7 @@ fn open_tab(
url: String,
) -> Result<()> {
let tab_id = state.alloc_id();
let epoch = state.alloc_epoch();
let chrome_h = chrome_height(state);
let tab_bounds = content_bounds(window, chrome_h, state.host_mode);
let (webview, effective_host_mode) = build_tab_webview(
@ -2138,6 +2173,7 @@ fn open_tab(
state.user_agent.as_deref(),
state.web_context.as_mut(),
tab_id,
epoch,
&url,
tab_bounds,
)?;
@ -2149,6 +2185,7 @@ fn open_tab(
url,
kir_enabled: false,
webview: Some(webview),
epoch,
back_stack: Vec::new(),
forward_stack: Vec::new(),
pending_nav_from_stack: false,
@ -2227,6 +2264,7 @@ fn ensure_tab_loaded(
let chrome_h = chrome_height(state);
let tab_bounds = content_bounds(window, chrome_h, state.host_mode);
let tab_id = state.tabs[index].id;
let epoch = state.alloc_epoch();
let tab_url = state.tabs[index].url.clone();
let (webview, effective_host_mode) = build_tab_webview(
window,
@ -2238,18 +2276,23 @@ fn ensure_tab_loaded(
state.user_agent.as_deref(),
state.web_context.as_mut(),
tab_id,
epoch,
&tab_url,
tab_bounds,
)?;
state.host_mode = effective_host_mode;
state.tabs[index].webview = Some(webview);
state.tabs[index].epoch = epoch;
Ok(())
}
fn suspend_background_tabs(state: &mut BrowserState) {
for (i, tab) in state.tabs.iter_mut().enumerate() {
if i != state.active && !tab.kir_enabled {
if i != state.active && !tab.kir_enabled && tab.webview.is_some() {
tab.webview = None;
// Invalidate pending events from the destroyed webview so they
// cannot overwrite this tab's url/title while it is suspended.
tab.epoch = 0;
}
}
}