From 51abf79184f69f219d6d0cbbdc099e20fe258f48 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 6 Jun 2026 16:14:56 -0400 Subject: [PATCH 1/2] fix: retry transient product capture navigation --- internal/provider/provider.go | 28 +++++++++++++++++++++++++++- internal/provider/provider_test.go | 18 ++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 72f522b..b98f66f 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)); + 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..e9d0e83 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -427,6 +427,24 @@ 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 });", + } { + if !strings.Contains(playwrightCaptureScript, required) { + t.Fatalf("playwright script must retry transient navigation failure path %q", required) + } + } + if strings.Contains(playwrightCaptureScript, "manual review') && isTransientNavigationError") { + t.Fatal("playwright script must not retry manual-review CAPTCHA/interstitial failures") + } +} + func containsString(values []string, want string) bool { for _, value := range values { if value == want { From 44449f147e9f585dba74ba27c92e6bb464087c3e Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 6 Jun 2026 16:18:51 -0400 Subject: [PATCH 2/2] fix: harden navigation retry review feedback --- internal/provider/provider.go | 2 +- internal/provider/provider_test.go | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b98f66f..838e576 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -431,7 +431,7 @@ async function launchChromeBrowser() { } function isTransientNavigationError(err) { - const message = err && (err.stack || err.message || String(err)); + const message = err && (err.stack || err.message) ? String(err.stack || err.message) : String(err); return [ 'net::ERR_NETWORK_CHANGED', 'net::ERR_NETWORK_RESET', diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index e9d0e83..f8dc8fd 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -435,13 +435,16 @@ func TestPlaywrightScriptRetriesTransientNavigationFailures(t *testing.T) { "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) } } - if strings.Contains(playwrightCaptureScript, "manual review') && isTransientNavigationError") { - t.Fatal("playwright script must not retry manual-review CAPTCHA/interstitial failures") + 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") } }