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) + }) +})