diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 536a71ca..64ff2234 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -282,9 +282,42 @@ function publicAuth(value) { async function gotoTarget(rawTarget) { const target = new URL(String(rawTarget || targetPath), baseUrl).toString(); const beforeUrl = currentPageUrl(); - const response = await page.goto(target, { waitUntil: "domcontentloaded", timeout: 30000 }); - await page.waitForTimeout(1000).catch(() => {}); - return { beforeUrl, afterUrl: currentPageUrl(), httpStatus: response ? response.status() : null, pageId }; + const attempts = []; + for (let attempt = 1; attempt <= 3; attempt += 1) { + try { + const response = await page.goto(target, { waitUntil: "domcontentloaded", timeout: 30000 }); + await page.waitForTimeout(1000).catch(() => {}); + attempts.push({ attempt, ok: true, httpStatus: response ? response.status() : null }); + return { beforeUrl, afterUrl: currentPageUrl(), httpStatus: response ? response.status() : null, pageId, attempts }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + attempts.push({ attempt, ok: false, failureKind: navigationFailureKind(message), message: redactErrorMessage(message) }); + if (attempt >= 3 || !isRetryableNavigationError(message)) { + throw Object.assign(new Error(message), { attempts, target }); + } + await page.waitForTimeout(500 * attempt).catch(() => {}); + } + } + return { beforeUrl, afterUrl: currentPageUrl(), httpStatus: null, pageId, attempts }; +} + +function isRetryableNavigationError(message) { + return /net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|net::ERR_CONNECTION_RESET|Navigation timeout/iu.test(String(message || "")); +} + +function navigationFailureKind(message) { + const text = String(message || ""); + if (/net::ERR_NETWORK_CHANGED/iu.test(text)) return "net::ERR_NETWORK_CHANGED"; + if (/net::ERR_ABORTED/iu.test(text)) return "net::ERR_ABORTED"; + if (/net::ERR_CONNECTION_RESET/iu.test(text)) return "net::ERR_CONNECTION_RESET"; + if (/Navigation timeout/iu.test(text)) return "navigation-timeout"; + return "navigation-error"; +} + +function redactErrorMessage(message) { + return String(message || "") + .replace(/([?&](?:token|key|password|secret|authorization)=)[^&\s]+/giu, "$1[redacted]") + .replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gu, "$1[redacted]"); } async function createSessionFromUi() {