diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 72f522b..838e576 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -430,6 +430,32 @@ async function launchChromeBrowser() { }); } +function isTransientNavigationError(err) { + const message = err && (err.stack || err.message) ? String(err.stack || err.message) : String(err); + return [ + 'net::ERR_NETWORK_CHANGED', + 'net::ERR_NETWORK_RESET', + 'net::ERR_TIMED_OUT', + ].some((needle) => message.includes(needle)); +} + +async function gotoWithTransientRetry(page, url, timeout) { + let lastErr; + for (let attempt = 1; attempt <= 3; attempt++) { + try { + await page.goto(url, { waitUntil: 'domcontentloaded', timeout }); + return; + } catch (err) { + lastErr = err; + if (attempt === 3 || !isTransientNavigationError(err)) { + throw err; + } + await page.waitForTimeout(500 * attempt); + } + } + throw lastErr; +} + async function main() { const url = process.argv[2]; const timeout = Number(process.argv[3] || 45000); @@ -441,7 +467,7 @@ async function main() { Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); }); try { - await page.goto(url, { waitUntil: 'domcontentloaded', timeout }); + await gotoWithTransientRetry(page, url, timeout); if (await page.locator('form[action*="/errors/validateCaptcha"]').count()) { throw new Error('amazon interstitial requires manual review'); } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 769d379..f8dc8fd 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -427,6 +427,27 @@ func TestPlaywrightScriptWaitsForCaptureRelevantNodes(t *testing.T) { } } +func TestPlaywrightScriptRetriesTransientNavigationFailures(t *testing.T) { + for _, required := range []string{ + "isTransientNavigationError", + "net::ERR_NETWORK_CHANGED", + "net::ERR_NETWORK_RESET", + "net::ERR_TIMED_OUT", + "for (let attempt = 1; attempt <= 3; attempt++)", + "await page.goto(url, { waitUntil: 'domcontentloaded', timeout });", + "String(err)", + } { + if !strings.Contains(playwrightCaptureScript, required) { + t.Fatalf("playwright script must retry transient navigation failure path %q", required) + } + } + retryIndex := strings.Index(playwrightCaptureScript, "await gotoWithTransientRetry(page, url, timeout);") + captchaIndex := strings.Index(playwrightCaptureScript, `form[action*="/errors/validateCaptcha"]`) + if retryIndex < 0 || captchaIndex < 0 || captchaIndex < retryIndex { + t.Fatal("playwright script must check CAPTCHA/interstitials after retryable navigation only") + } +} + func containsString(values []string, want string) bool { for _, value := range values { if value == want {