Skip to content

fix(TE-23055): bound page/context teardown so a crashed target cannot hang the web scanner build - #546

Open
shrinishLT wants to merge 1 commit into
LambdaTest:stagefrom
shrinishLT:TE-23055
Open

fix(TE-23055): bound page/context teardown so a crashed target cannot hang the web scanner build#546
shrinishLT wants to merge 1 commit into
LambdaTest:stagefrom
shrinishLT:TE-23055

Conversation

@shrinishLT

@shrinishLT shrinishLT commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Problem

Web scanner runs against jobs.prudential.com never finished — the build stayed running until the job timed out (reported as "scan taking ~30 minutes"), and no finalizeBuild was ever sent.

Root cause

The site is behind Imperva and rate-limits the scan. Once limiting kicks in, the WAF serves a challenge/redirect that destroys the page's execution context mid-scroll, and the browser target then crashes:

screenshot capture failed for viewport 1920 ... page.evaluate: Execution context was destroyed
screenshot capture failed for viewport 1366 ... page.goto: Page crashed
screenshot capture failed for viewport 1028 ... page.setViewportSize: Target crashed

All viewports fail, so captureScreenshotsForConfig throws — and its finally runs await page.close(); await context.close(). On a crashed target those never resolve. The capture promise is stranded, so Promise.allSettled in captureScreenshotsAsync never settles, captureScreenshots never returns, and finalizeBuild is unreachable.

Evidence from an instrumented run: 12 × closing page/context vs 11 × page/context closed — exactly one teardown never came back.

Note exitOnError: false on the capture task does not help here: it rescues a task that throws, not one that hangs.

Fix

Race the teardown against BROWSER_CLOSE_TIMEOUT (15s) and continue if it doesn't return. Abandoning a wedged page/context is safe because closeBrowsers() force-kills the browser process immediately afterwards.

  • screenshot.ts — bounded teardown in the finally, with a warning when it times out
  • constants.tsBROWSER_CLOSE_TIMEOUT: 15000

The happy path is unchanged: a healthy close resolves in milliseconds, so no warning and no behavioural difference.

How BROWSER_CLOSE_TIMEOUT was chosen

The instinct is to use the maximum of the waits we already have. That's the wrong rule here, because every existing wait bounds page work, whereas this bounds teardown of a page we're already finished with.

Existing waits/timeouts in the capture path

Wait Value What it bounds
waitForPageRender / DEFAULT_PAGE_LOAD_TIMEOUT 180,000 ms (floored to 30,000 when set) page.goto — network + render
waitForTimeout (config / per-URL) 1,000–5,000 ms settle pause after scrolling
scrollToBottomAndBackToTop 100 × scrollTime (8) ≈ 800 ms in-page full-page scroll
smoothScrollToBottom (lazy load) maxScrolls 50 × scrollDelay 300 ≈ 15,000 ms in-page lazy-load scroll
humanLikeScroll 2000+300+300+2000 = 4,600 ms mouse-movement simulation
waitForDiscovery 30,000 ms discovery path (not capture)
Route fetch / axios 30,000 ms network requests, uploads

Measured cost of a healthy close (fully-loaded, fully-scrolled page at 1920×1080)

Engine page.close() context.close() Total
chromium 8 ms 2 ms 10 ms
firefox 3 ms 15 ms 18 ms
webkit 18 ms 12 ms 30 ms

Playwright doesn't run beforeunload on close (runBeforeUnload defaults to false), doesn't wait for in-flight network, and capture enables no video/HAR/trace recording — artifact flush being the only thing that legitimately makes context.close() slow. So a healthy close is ≤30 ms, three orders of magnitude under the chosen value.

Taking the max (180,000 ms) would mean 6 concurrent browser configs each waiting 3 minutes on a target already known dead — up to 3 extra minutes of dead wait per run, on exactly the runs this PR is meant to speed up. The value isn't "how long might this legitimately take" (≈30 ms) but "how long before we conclude the target is dead". 15,000 ms is ~500× the worst measured close and matches the largest in-page wait in the system. 5,000 ms would also be defensible; 15,000 ms leaves margin for a loaded CI host running six browsers.

Caveats: measurements are macOS/unloaded (Linux CI will be slower, by a small multiple — not 500×); and if video/HAR/trace recording is ever enabled for capture, this constant must be revisited.

Scope — what this does and does not fix

Does fix: the hang. The build now finalizes and completes quickly instead of running until the job timeout.

Does not fix: the screenshots themselves. On rate-limited sites the first few captures succeed and subsequent ones still return Access Denied block pages. That is the customer's WAF rate limiting, and it can only be resolved once the HYE VMs are whitelisted by the customer.

Verification

  • Instrumented run reproduced the hang and pinpointed the unmatched close().
  • With the fix, builds reach completed (verified on stage webscanner runs, and on two back-to-back A/B runs).
  • No behaviour change on healthy captures.

RFC: LambdatestIncPrivate/internal-docs#2624

🤖 Generated with Claude Code

… hang the build

On rate-limited/WAF-protected sites the target can crash mid-capture (page.goto:
Page crashed / setViewportSize: Target crashed). page.close()/context.close()
then never resolve on the dead target, so the capture promise never settles,
Promise.allSettled never returns and finalizeBuild is unreachable — the build
stays 'running' until the job times out (seen as ~30 minute scans). Evidence
from an instrumented run: 12 'closing page/context' vs 11 'page/context closed'.

Race the teardown against BROWSER_CLOSE_TIMEOUT (15s) and continue if it does
not return; closeBrowsers() force-kills the browser process afterwards.

Scope: this fixes only the hang, so the build completes quickly. Screenshots may
still show Access Denied after the first few captures — that is the customer's
WAF rate limiting and is resolved only once the HYE VMs are whitelisted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants