karatelabs/karate · error · RuntimeException
page load timeout after
Error message
page load timeout after {ms}ms - url: {url}, strategy: {strategy}, domContentEventFired: {bool}, framesStillLoading: {bool}, mainFrameId: {id}, expectedLoaderId: {id}, supersededLoaderId: {id}, committedLoaderId: {id}, domContentLoaderId: {id}, readyState: {state}, jsExec: {state}, cdpOpen: {bool} What it means
Karate waited for a page load (navigation) until the configured timeout and the load did not complete. The message embeds a rich diagnostic: whether domContentEventFired fired, frames still loading, the main frame id, and the progression of loader ids (expected/superseded/committed/domContent) plus document readyState, JS-executability and CDP connection state, to pinpoint where loading stalled.
Solutions
- Increase the driver timeout (e.g. configure options.timeout or karate driver config) so slow pages can finish loading
- Change the waitUntil strategy (e.g. from 'load' to 'domcontentloaded') if a late subresource blocks the load event
- Open the diagnostic fields: framesStillLoading=true points at an iframe/hung subresource; committedLoaderId null means navigation never committed (check network/DNS/TLS)
- Check the page manually in a browser at the same network path to identify blocking resources
Example fix
// before
DriverOptions: { type:'chromium', timeout: 30000, waitForLoad:'load' }
// after
DriverOptions: { type:'chromium', timeout: 90000, waitForLoad:'domcontentloaded' } Defensive patterns
Strategy: try-catch
Validate before calling
// pre-check reachability before navigation
Http.Response r = Http.forUrl(url).header("Connection","close").get();
if (r.getStatus() >= 400) throw new IllegalStateException("target url unhealthy: " + r.getStatus()); Try / catch
try { driver.setUrl(url); } catch (RuntimeException e) { if (e.getMessage().startsWith("page load timeout")) { logger.warn("load stalled: {}", e.getMessage()); driver.setUrl(url); // one retry } else throw e; } Prevention
- Size the driver timeout to the slowest environment, not local dev
- Prefer domcontentloaded strategy for pages with long-loading subresources
- Block known-slow third-party domains via proxy or hosts entries
- Read the diagnostic fields (framesStillLoading, committedLoaderId) to target the real stall cause
When it happens
Trigger: driver.setUrl(url) or navigation with waitUntil strategy where, after the timeout duration, DOMContentLoaded never fired, or frames remained in loading state, or the loader chain shows a superseded navigation, or the document never reached the expected readyState.
Common situations: Slow pages behind corporate proxies/VPN; ad-blockers or CSP blocking critical resources; pages that never finish loading (long-polling, streaming media); misjudged waitUntil strategy or too-low driver timeout config; servers hanging on a subresource.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Page.navigate timed out, retrying
- CDP timeout for
- timeout waiting for URL to contain
- waitForUrl timeout: expected URL containing
- readyState check failed
AI-assisted analysis of karatelabs/karate@a22eb90246 (2026-09-12).
Data as JSON: /api/errors/e3dbeb263658295f.
Report an issue: GitHub.
Appendix: source
Thrown at karate-core/src/main/java/io/karatelabs/driver/cdp/CdpDriver.java:1441
// timeout (or exceptional completion): fall through and re-check
}
}
// Build diagnostic message with comprehensive state info
String url = pendingNavigationUrl != null ? pendingNavigationUrl : "(unknown)";
String readyStateInfo = getReadyStateForDiagnostic();
String jsExecInfo = getJsExecStateForDiagnostic();
String diagnostic = String.format(
"page load timeout after %dms - url: %s, strategy: %s, " +
"domContentEventFired: %s, framesStillLoading: %s, mainFrameId: %s, " +
"expectedLoaderId: %s, supersededLoaderId: %s, committedLoaderId: %s, domContentLoaderId: %s, " +
"readyState: %s, jsExec: %s, cdpOpen: %s",
timeout.toMillis(), url, strategy,
domContentEventFired, framesStillLoading, mainFrameId,
expectedLoaderId, supersededLoaderId, committedLoaderId, domContentLoaderId,
readyStateInfo, jsExecInfo, cdp.isOpen());
logger.warn(diagnostic);
throw new RuntimeException(diagnostic);
}
/**
* Verify that JS execution works in the context script() will actually use.
* <p>
* The page is only truly "loaded" once the main frame's default execution context
* is live - that is the single thing the readiness future tracks. We await it
* briefly (it is usually already complete; during a navigation swap it settles in
* milliseconds) and probe THAT context.
* <p>
* Crucially, an <i>error</i> from the explicit-context probe is NOT taken as "JS not
* ready": {@link #mainContextReady} can hand out a contextId that a loader
* replacement has already torn down when the matching executionContextsCleared was
* never delivered (routine under CI load). The stale id then errors forever and, on
* a fully-loaded page, wedges waitForPageLoad() to a 30s timeout — observed in CI as
* "page load complete but JS context not ready yet" while the timeout diagnostic
* reports {@code jsExec: ok} (it probes the default context). So on an explicit-
* context error we re-probe the default context: this is a liveness checkView on GitHub (pinned to a22eb90246)