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'
|
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 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 {
|
interface WindowNavigationState {
|
||||||
readyForDetection: boolean
|
readyForDetection: boolean
|
||||||
lastMainFrameError: string | null
|
lastMainFrameError: string | null
|
||||||
|
blankPageWatchdog: NodeJS.Timeout | null
|
||||||
|
blankPageRetried: boolean
|
||||||
|
context: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigationStateRegistry = new WeakMap<BrowserWindow, WindowNavigationState>()
|
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 {
|
function escapeHtml(value: string): string {
|
||||||
return value
|
return value
|
||||||
.replaceAll('&', '&')
|
.replaceAll('&', '&')
|
||||||
@@ -126,6 +205,9 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
|||||||
navigationStateRegistry.set(window, {
|
navigationStateRegistry.set(window, {
|
||||||
readyForDetection: false,
|
readyForDetection: false,
|
||||||
lastMainFrameError: null,
|
lastMainFrameError: null,
|
||||||
|
blankPageWatchdog: null,
|
||||||
|
blankPageRetried: false,
|
||||||
|
context,
|
||||||
})
|
})
|
||||||
|
|
||||||
window.webContents.on('did-start-loading', () => {
|
window.webContents.on('did-start-loading', () => {
|
||||||
@@ -134,6 +216,7 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
state.readyForDetection = false
|
state.readyForDetection = false
|
||||||
|
clearBlankPageWatchdog(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
window.webContents.on(
|
window.webContents.on(
|
||||||
@@ -147,6 +230,7 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
|||||||
if (state) {
|
if (state) {
|
||||||
state.readyForDetection = false
|
state.readyForDetection = false
|
||||||
state.lastMainFrameError = `${errorCode}:${errorDescription}`
|
state.lastMainFrameError = `${errorCode}:${errorDescription}`
|
||||||
|
clearBlankPageWatchdog(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(`[desktop-window] ${context} main-frame load failed`, {
|
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)
|
state.readyForDetection = !currentURL.startsWith(errorPagePrefix)
|
||||||
if (state.readyForDetection) {
|
if (state.readyForDetection) {
|
||||||
state.lastMainFrameError = null
|
state.lastMainFrameError = null
|
||||||
|
scheduleBlankPageWatchdog(window, state)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -174,10 +259,18 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
|
|||||||
const state = navigationStateRegistry.get(window)
|
const state = navigationStateRegistry.get(window)
|
||||||
if (state) {
|
if (state) {
|
||||||
state.readyForDetection = false
|
state.readyForDetection = false
|
||||||
|
clearBlankPageWatchdog(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`[desktop-window] ${context} renderer gone`, details)
|
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 {
|
export function isWindowReadyForDetection(window: BrowserWindow): boolean {
|
||||||
@@ -193,6 +286,12 @@ export async function loadWindowURLSafely(
|
|||||||
targetURL: string,
|
targetURL: string,
|
||||||
context: string,
|
context: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const state = navigationStateRegistry.get(window)
|
||||||
|
if (state) {
|
||||||
|
state.blankPageRetried = false
|
||||||
|
clearBlankPageWatchdog(state)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.loadURL(targetURL, { userAgent: STANDARD_USER_AGENT })
|
await window.loadURL(targetURL, { userAgent: STANDARD_USER_AGENT })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user