fix(desktop): keep oauth popups alive on redirect aborts and blank loads
Toutiao's wap_login redirect chain raises ERR_ABORTED (-3) without the literal symbol in the message, so the previous regex fell through to the fallback error page. Now match the numeric form too, and add a 7s blank- page watchdog that auto-reloads stuck "Loading..." popups once per fresh navigation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,16 +2,95 @@ import type { BrowserWindow } from 'electron/main'
|
||||
|
||||
import { STANDARD_USER_AGENT } from './user-agent'
|
||||
|
||||
const ignoredNavigationFailurePattern = /ERR_ABORTED|ERR_BLOCKED_BY_CLIENT/i
|
||||
const ignoredNavigationFailurePattern = /ERR_ABORTED|ERR_BLOCKED_BY_CLIENT|\((-3|-20)\) loading/i
|
||||
const errorPagePrefix = 'data:text/html'
|
||||
|
||||
const BLANK_PAGE_PROBE_DELAY_MS = 7_000
|
||||
const BLANK_PAGE_NODE_THRESHOLD = 3
|
||||
const BLANK_PAGE_TEXT_THRESHOLD = 5
|
||||
|
||||
interface WindowNavigationState {
|
||||
readyForDetection: boolean
|
||||
lastMainFrameError: string | null
|
||||
blankPageWatchdog: NodeJS.Timeout | null
|
||||
blankPageRetried: boolean
|
||||
context: string
|
||||
}
|
||||
|
||||
const navigationStateRegistry = new WeakMap<BrowserWindow, WindowNavigationState>()
|
||||
|
||||
function clearBlankPageWatchdog(state: WindowNavigationState): void {
|
||||
if (state.blankPageWatchdog) {
|
||||
clearTimeout(state.blankPageWatchdog)
|
||||
state.blankPageWatchdog = null
|
||||
}
|
||||
}
|
||||
|
||||
async function probePageBodyShape(
|
||||
window: BrowserWindow,
|
||||
): Promise<{ nodeCount: number; textLength: number; readyState: string } | null> {
|
||||
if (window.isDestroyed()) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return (await window.webContents.executeJavaScript(
|
||||
`(() => ({
|
||||
nodeCount: document.body ? document.body.querySelectorAll('*').length : 0,
|
||||
textLength: document.body ? (document.body.innerText || '').trim().length : 0,
|
||||
readyState: document.readyState,
|
||||
}))()`,
|
||||
true,
|
||||
)) as { nodeCount: number; textLength: number; readyState: string }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleBlankPageWatchdog(window: BrowserWindow, state: WindowNavigationState): void {
|
||||
clearBlankPageWatchdog(state)
|
||||
|
||||
state.blankPageWatchdog = setTimeout(() => {
|
||||
state.blankPageWatchdog = null
|
||||
if (window.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentURL = window.webContents.getURL()
|
||||
if (currentURL.startsWith(errorPagePrefix) || currentURL === 'about:blank') {
|
||||
return
|
||||
}
|
||||
|
||||
void probePageBodyShape(window).then((shape) => {
|
||||
if (!shape || window.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
const looksBlank =
|
||||
shape.nodeCount < BLANK_PAGE_NODE_THRESHOLD &&
|
||||
shape.textLength < BLANK_PAGE_TEXT_THRESHOLD
|
||||
|
||||
if (!looksBlank) {
|
||||
return
|
||||
}
|
||||
|
||||
if (state.blankPageRetried) {
|
||||
console.warn(`[desktop-window] ${state.context} still blank after auto-reload`, {
|
||||
url: currentURL,
|
||||
...shape,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
state.blankPageRetried = true
|
||||
console.warn(`[desktop-window] ${state.context} blank page detected, reloading once`, {
|
||||
url: currentURL,
|
||||
...shape,
|
||||
})
|
||||
window.webContents.reloadIgnoringCache()
|
||||
})
|
||||
}, BLANK_PAGE_PROBE_DELAY_MS)
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
@@ -126,6 +205,9 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
||||
navigationStateRegistry.set(window, {
|
||||
readyForDetection: false,
|
||||
lastMainFrameError: null,
|
||||
blankPageWatchdog: null,
|
||||
blankPageRetried: false,
|
||||
context,
|
||||
})
|
||||
|
||||
window.webContents.on('did-start-loading', () => {
|
||||
@@ -134,6 +216,7 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
||||
return
|
||||
}
|
||||
state.readyForDetection = false
|
||||
clearBlankPageWatchdog(state)
|
||||
})
|
||||
|
||||
window.webContents.on(
|
||||
@@ -147,6 +230,7 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
||||
if (state) {
|
||||
state.readyForDetection = false
|
||||
state.lastMainFrameError = `${errorCode}:${errorDescription}`
|
||||
clearBlankPageWatchdog(state)
|
||||
}
|
||||
|
||||
console.warn(`[desktop-window] ${context} main-frame load failed`, {
|
||||
@@ -167,6 +251,7 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
||||
state.readyForDetection = !currentURL.startsWith(errorPagePrefix)
|
||||
if (state.readyForDetection) {
|
||||
state.lastMainFrameError = null
|
||||
scheduleBlankPageWatchdog(window, state)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -174,10 +259,18 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
||||
const state = navigationStateRegistry.get(window)
|
||||
if (state) {
|
||||
state.readyForDetection = false
|
||||
clearBlankPageWatchdog(state)
|
||||
}
|
||||
|
||||
console.error(`[desktop-window] ${context} renderer gone`, details)
|
||||
})
|
||||
|
||||
window.once('closed', () => {
|
||||
const state = navigationStateRegistry.get(window)
|
||||
if (state) {
|
||||
clearBlankPageWatchdog(state)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function isWindowReadyForDetection(window: BrowserWindow): boolean {
|
||||
@@ -193,6 +286,12 @@ export async function loadWindowURLSafely(
|
||||
targetURL: string,
|
||||
context: string,
|
||||
): Promise<void> {
|
||||
const state = navigationStateRegistry.get(window)
|
||||
if (state) {
|
||||
state.blankPageRetried = false
|
||||
clearBlankPageWatchdog(state)
|
||||
}
|
||||
|
||||
try {
|
||||
await window.loadURL(targetURL, { userAgent: STANDARD_USER_AGENT })
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user