fix(stage-tamagotchi): ERR_ABORT (-3) occurs when loading hash route window

This commit is contained in:
Neko Ayaka
2026-02-26 14:45:07 +08:00
parent bbf40d28fe
commit 3a107b7465
2 changed files with 105 additions and 9 deletions
@@ -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` })
})
})
@@ -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 }
}