chenglou/pretext · error · Error
Timed out waiting for posted report from ${browser} (last ph
Error message
Timed out waiting for posted report from ${browser} (last phase: ${lastPhase}) What it means
Thrown by loadPostedReport — the POST-sidechannel sibling of [23]. The host starts a local HTTP server (startPostedReportServer), hands its endpoint to the page, navigates, then races waitForReport() against a timeoutMs poll loop. If neither resolves nor rejects before the loop exhausts (and no earlier reportError threw), the harness gives up and reports the last navigation phase. Unlike [23], the report arrives via fetch POST, so size is not the issue — reachability and page execution are.
Source
Thrown at scripts/browser-automation.ts:714
if (reportError !== null) {
throw reportError
}
const phase = await readLastNavigationPhase(session, expectedRequestId)
if (phase !== null) {
lastPhase = phase
}
}
if (resolvedReport !== null) {
return resolvedReport
}
if (reportError !== null) {
throw reportError
}
const observedUrl = formatObservedLocation(await session.readLocationUrl())
throw new Error(getTimeoutMessage(browser, 'posted report', lastPhase, observedUrl))
}
View on GitHub (pinned to ac49b09b7d)
Solutions
- Distinguish from [23]: this is the POST path, so report size is not the cause — focus on reachability (CORS, firewall, endpoint URL).
- Read lastPhase: "posting" with no report means the page tried to POST but the server never received it — check the report server is bound to an address reachable from the browser and emits permissive CORS headers.
- Confirm the reportEndpoint query parameter in the URL exactly matches startPostedReportServer's endpoint (encoding mismatch is the most common silent failure).
- Watch the page's network tab in a headed run: a failed POST (blocked/CORS/404) shows up there even though loadPostedReport only sees silence.
- Raise --timeout if the page is genuinely slow (large sweep) rather than blocked.
Example fix
// before
const report = await loadPostedReport(session, url, () => reportServer.waitForReport(null), requestId, browser, timeoutMs)
// after — bind the report server to all interfaces only if the browser cannot reach 127.0.0.1, and log endpoint mismatch
const reportServer = await startPostedReportServer<CorpusReport>(requestId)
const urlWithEndpoint = `${url}&reportEndpoint=${encodeURIComponent(reportServer.endpoint)}`
if (!urlWithEndpoint.includes(`reportEndpoint=${encodeURIComponent(reportServer.endpoint)}`)) {
throw new Error(`reportEndpoint encoding mismatch; page will POST to the wrong URL`)
}
const report = await loadPostedReport(session, urlWithEndpoint, () => reportServer.waitForReport(null), requestId, browser, timeoutMs) Defensive patterns
Strategy: try-catch
Validate before calling
// Verify the reportEndpoint is reachable from the browser BEFORE navigating
async function canReachReportEndpoint(endpoint: string): Promise<boolean> {
try {
const res = await fetch(endpoint, { method: 'OPTIONS' })
return res.ok || res.status === 404 // 404 is fine — server exists, just no OPTIONS route
} catch {
return false
}
}
if (!(await canReachReportEndpoint(reportServer.endpoint))) {
throw new Error(`reportEndpoint ${reportServer.endpoint} not reachable; the page POST will never arrive.`)
} Try / catch
try {
return await loadPostedReport(session, urlWithEndpoint, () => reportServer.waitForReport(null), requestId, browser, timeoutMs)
} catch (error) {
if (error instanceof Error && error.message.includes('last phase: posting')) {
// POST never landed — re-verify endpoint/CORS rather than retrying blindly
throw new Error(`${error.message}. Verify reportEndpoint reachability and CORS for ${reportServer.endpoint}.`)
}
throw error
} Prevention
- URL-encode reportServer.endpoint into the page URL — a bare `&` or `:` will silently redirect the POST.
- Ensure startPostedReportServer binds to an address the browser can reach (127.0.0.1 usually fine locally; not if the browser is remote/headless on another host).
- Confirm the report server emits permissive CORS headers; cross-origin POSTs from the page otherwise fail preflight.
- Keep requestId unique per attempt so a late POST from a previous run cannot satisfy the current waitForReport.
When it happens
Trigger: loadPostedReport kicks off waitForReport() (draining the POST server) and concurrently polls readLastNavigationPhase every 100ms. Throw when the loop ends with resolvedReport === null && reportError === null. Concrete causes: the page never ran its POST because it errored before the fetch (lastPhase "measuring"); the POST hit a CORS or mixed-content block (server is http://localhost, page is file:// or a different origin); the reportEndpoint query param was malformed so the page POSTed the wrong URL; a firewall/proxy intercepted localhost; the browser tab was closed mid-flight so the fetch never fired.
Common situations: Firefox/Safari blocking fetch to a different origin without CORS headers on the report server; a corporate proxy or OS firewall redirecting localhost traffic; the page's report-post code path has a bug for a specific corpus; the report server was started on a port the browser cannot reach (binding to 127.0.0.1 vs the address the page resolves); race where the page POSTs before startPostedReportServer is listening (the waitForReport promise rejects but the rejection is caught into reportError — distinct from this throw).
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Timed out waiting for local port ${port}
- Timed out waiting for report from ${browser} (last phase: ${
- Timed out waiting for local Bun server on ${baseUrl}
- Timed out waiting for ${browser} automation lock
- ${navigate.message ?? navigate.error}
AI-assisted analysis of chenglou/pretext@ac49b09b7d (2026-08-12).
Data as JSON: /api/errors/52d564aa26a71ef3.
Report an issue: GitHub.