From c06a8e00d9573f673d1b2b5a77c37ac72a468df6 Mon Sep 17 00:00:00 2001 From: HuangLeijiana <2644901977@qq.com> Date: Sun, 6 Sep 2026 00:13:31 +0800 Subject: [PATCH] fix(main): ignore ERR_ABORTED did-fail-load events in renderer recovery Programmatic navigation switches (restart, recovery page, Safe Mode toggle, second-instance reopen) stop the in-flight load before loading the next URL, which Electron reports as did-fail-load with the numeric net error ERR_ABORTED (-3). The recovery handler treated every such event as a renderer loss: it issued an extra webContents.reload() that raced the navigation it just started and consumed the shared 3-reload budget, so a genuine crash later hit the throttled branch and surfaced the 'stopped responding' recovery UI over a healthy runtime. openHarness already treats ERR_ABORTED as benign at the loadURL await site; mirror that here via a new isAbortedNavigationCode predicate and cover both the numeric-code and Error shapes with unit tests. --- src/main/index.ts | 9 +++++++++ src/main/window-navigation.ts | 11 +++++++++++ test/window-navigation.test.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 test/window-navigation.test.ts diff --git a/src/main/index.ts b/src/main/index.ts index d4ea8115..a3c5180c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -98,6 +98,7 @@ import { import { clearStaleHarnessAuthCookies, desktopHarnessUrl, + isAbortedNavigationCode, isAbortedNavigationError, shouldLoadHarnessUrl } from './window-navigation' @@ -387,6 +388,14 @@ function installMainWindowRendererRecovery(window: BrowserWindow): void { }) webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { if (!isMainFrame) return + // A programmatic switch (restart, recovery page, Safe Mode toggle, + // second-instance reopen) calls webContents.stop() before the next + // loadFile/loadURL, and Electron reports the cancelled in-flight load as + // did-fail-load with ERR_ABORTED (-3). That is a benign supersession — + // openHarness treats it the same way at the loadURL await site — so do not + // spend a recovery reload (or the shared reload budget) on a navigation + // nobody cancelled on purpose. + if (isAbortedNavigationCode(errorCode)) return clearProfileBootConfirmation() // The harness web server is local; a failure to reach it is almost // always the renderer dropping, not a real network error. Surface the diff --git a/src/main/window-navigation.ts b/src/main/window-navigation.ts index c080bbd6..22e896f2 100644 --- a/src/main/window-navigation.ts +++ b/src/main/window-navigation.ts @@ -83,3 +83,14 @@ export function isAbortedNavigationError(error: unknown): boolean { /(?:^|\s)ERR_ABORTED\s*\(-3\)(?:\s|$)/.test(navigationError.message) ) } + +/** + * Chromium reports a cancelled in-flight load as `did-fail-load` with the + * numeric net error ERR_ABORTED (-3). `webContents.stop()` followed by a new + * load — restart flows, recovery pages, Safe Mode toggles, second-instance + * reopens — cancels the previous navigation that way, so the code above is not + * enough for the event shape, which carries `errorCode: number`. + */ +export function isAbortedNavigationCode(errorCode: number): boolean { + return errorCode === -3 +} diff --git a/test/window-navigation.test.ts b/test/window-navigation.test.ts new file mode 100644 index 00000000..e9d078a9 --- /dev/null +++ b/test/window-navigation.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import { + isAbortedNavigationCode, + isAbortedNavigationError +} from '../src/main/window-navigation' + +describe('aborted-navigation detection', () => { + it('recognizes the ERR_ABORTED numeric code reported by did-fail-load', () => { + expect(isAbortedNavigationCode(-3)).toBe(true) + }) + + it('does not treat real load failures as aborted navigations', () => { + // -105 is ERR_NAME_NOT_RESOLVED, -106 is ERR_INTERNET_DISCONNECTED, + // 0 means "no error" — none of them is a superseded load. + expect(isAbortedNavigationCode(-105)).toBe(false) + expect(isAbortedNavigationCode(-106)).toBe(false) + expect(isAbortedNavigationCode(0)).toBe(false) + }) + + it('recognizes the Error shapes thrown by webContents.loadURL', () => { + expect(isAbortedNavigationError({ code: 'ERR_ABORTED' })).toBe(true) + expect(isAbortedNavigationError({ errno: -3 })).toBe(true) + expect(isAbortedNavigationError(new Error('ERR_ABORTED (-3)'))).toBe(true) + expect(isAbortedNavigationError(new Error('ERR_NAME_NOT_RESOLVED (-105)'))).toBe(false) + }) +})