From 3a107b746524fd25fcc65982629fc45379cfffb0 Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Thu, 26 Feb 2026 14:45:07 +0800 Subject: [PATCH] fix(stage-tamagotchi): ERR_ABORT (-3) occurs when loading hash route window --- .../src/main/libs/electron/location.test.ts | 28 ++++++ .../src/main/libs/electron/location.ts | 86 +++++++++++++++++-- 2 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 apps/stage-tamagotchi/src/main/libs/electron/location.test.ts diff --git a/apps/stage-tamagotchi/src/main/libs/electron/location.test.ts b/apps/stage-tamagotchi/src/main/libs/electron/location.test.ts new file mode 100644 index 000000000..44e5e6f3c --- /dev/null +++ b/apps/stage-tamagotchi/src/main/libs/electron/location.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it, vi } from 'vitest' + +import { withHashRoute } from './location' + +vi.mock(import('@electron-toolkit/utils'), () => { + return { + is: { + dev: true, + }, + } +}) + +describe('withHashRoute', () => { + it('should use string url construct URL with hash route correctly', () => { + const result = withHashRoute('http://localhost:5173', '/test/inner-test') + expect(result).toEqual({ url: 'http://localhost:5173/#/test/inner-test' }) + }) + + it('should use object url construct URL with hash route correctly', () => { + const result = withHashRoute({ url: 'http://localhost:5173' }, '/test/inner-test') + expect(result).toEqual({ url: 'http://localhost:5173/#/test/inner-test' }) + }) + + it('should use file url construct URL with hash route correctly', () => { + const result = withHashRoute({ url: 'file:////home/workspace/project/index.html' }, '/test/inner-test') + expect(result).toEqual({ url: `file:////home/workspace/project/index.html#/test/inner-test` }) + }) +}) diff --git a/apps/stage-tamagotchi/src/main/libs/electron/location.ts b/apps/stage-tamagotchi/src/main/libs/electron/location.ts index 444208c5b..5e4e97021 100644 --- a/apps/stage-tamagotchi/src/main/libs/electron/location.ts +++ b/apps/stage-tamagotchi/src/main/libs/electron/location.ts @@ -34,23 +34,91 @@ export function baseUrl(parentOfIndexHtml: string, filename?: string) { } export async function load(window: BrowserWindow, url: string | { url: string, options?: LoadURLOptions } | { file: string, options?: LoadFileOptions }) { - if (typeof url === 'object' && 'url' in url) { - return await window.loadURL(url.url, url.options) - } - if (typeof url === 'object' && 'file' in url) { - return await window.loadFile(url.file, url.options) - } + try { + if (typeof url === 'object' && 'url' in url) { + return await window.loadURL(url.url, url.options) + } + if (typeof url === 'object' && 'file' in url) { + return await window.loadFile(url.file, url.options) + } - return await window.loadURL(url) + return await window.loadURL(url) + } + catch (error) { + if (!(error instanceof Error)) { + throw error + } + + // Electron navigation error shape + // https://github.com/electron/electron/blob/8d05285a1f39c759985b17c89a449e4a6b3960df/lib/browser/api/web-contents.ts#L354-L359 + if (!('code' in error) || !('errno' in error)) { + throw error + } + if (error.code === 'ERR_ABORTED' && error.errno === -3) { + if (typeof url === 'object' && 'url' in url) { + const parsedURL = new URL(url.url) + if (parsedURL.hash) { + // When targeting /#/ hash route, Electron may throw + // + // ``` + // Error: ERR_ABORTED (-3) loading 'http://localhost:5173/#/notice/fade-on-hover?id=fade-on-hover' + // ``` + // + // and this will cause the `load(...)` promise to reject, while `#${hash content}` is in fact the correct URL expected by + // electron, but from `new URL(...)` standard API, the output URL with hash will include at least one `/` before `#${hash content}`, + // which causes the mismatch and thus the error. + // + // This is more likely a URL scheme standard mismatch between Electron and Node.js, and currently we can only catch and + // ignore this error, since the URL with hash is actually loaded correctly in Electron, and the error is just a false alarm. + // + // Navigation started: {url: 'http://localhost:5173/#/notice/fade-on-hover?id=fade-on-hover', isSameDocument: false, isMainFrame: true, isInPlace: false} + // Navigation started: {url: 'http://localhost:5173/#/notice/fade-on-hover?id=fade-on-hover', isSameDocument: false, isMainFrame: true, isInPlace: false} + // Navigation started: {url: 'http://localhost:5173/#/notice/fade-on-hover?id=fade-on-hover', isSameDocument: false, isMainFrame: true, isInPlace: false} + // + // https://github.com/electron/electron/issues/17526 + // https://github.com/electron/electron/blob/8d05285a1f39c759985b17c89a449e4a6b3960df/lib/browser/api/web-contents.ts#L370-L387 + console.warn('Electron navigation error with hash route, ignoring:', error, 'url:', url.url) + + return + } + } + } + + throw error + } + finally { + window.webContents.removeAllListeners('did-start-navigation') + } } +/** + * A helper function to construct URL with hash route, which is commonly used in our app since we are using hash-based routing in renderer. + */ export function withHashRoute(baseUrl: string | { url: string } | { file: string }, hashRoute: string) { if (typeof baseUrl === 'object' && 'url' in baseUrl) { - return { url: `${baseUrl.url}/#${hashRoute}` } satisfies { url: string, options?: LoadURLOptions } + // trim `/` suffix + const baseURLinURL = new URL(baseUrl.url) + + const pathname = baseURLinURL.pathname + const trimmedPathname = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname + baseURLinURL.pathname = trimmedPathname + + baseURLinURL.hash = hashRoute + + return { url: baseURLinURL.toString() } satisfies { url: string, options?: LoadURLOptions } } if (typeof baseUrl === 'object' && 'file' in baseUrl) { return { file: `${baseUrl.file}`, options: { hash: hashRoute } } satisfies { file: string, options?: LoadFileOptions } } - return `${baseUrl}/#${hashRoute}` + // trim `/` suffix + const baseURLinURL = new URL(baseUrl) + + const pathname = baseURLinURL.pathname + const trimmedPathname = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname + baseURLinURL.pathname = trimmedPathname + + baseURLinURL.hash = hashRoute + + return { url: baseURLinURL.toString() } satisfies { url: string, options?: LoadURLOptions } }