chenglou/pretext · error · Error
Timed out waiting for report from ${browser} (last phase: ${
Error message
Timed out waiting for report from ${browser} (last phase: ${lastPhase}) What it means
Thrown by loadHashReport when the page-side harness is expected to write its result into location.hash (the #report=... transport) and no matching report arrives within timeoutMs (default 60s, overridable via --timeout). The host navigates the session to a URL, then polls session.readLocationUrl() every 100ms, parsing the hash for a navigation phase and a report JSON. The error includes the last observed phase (loading / measuring / posting) so the caller can tell how far the page got.
Source
Thrown at scripts/browser-automation.ts:664
phase !== null &&
(phase.requestId === undefined || phase.requestId === expectedRequestId)
) {
lastPhase = phase.phase
}
const reportJson = readNavigationReportText(currentUrl)
if (reportJson === '' || reportJson === 'null') continue
const report = JSON.parse(reportJson) as T
if (report.requestId === expectedRequestId) {
return report
}
}
if (lastPhase === null) {
lastPhase = await readLastNavigationPhase(session, expectedRequestId)
}
const observedUrl = formatObservedLocation(await session.readLocationUrl())
throw new Error(getTimeoutMessage(browser, 'report', lastPhase, observedUrl))
}
export async function loadPostedReport<T extends { requestId?: string }>(
session: BrowserSession,
url: string,
waitForReport: () => Promise<T>,
expectedRequestId: string,
browser: BrowserKind,
timeoutMs = 60_000,
): Promise<T> {
await session.navigate(url)
let resolvedReport: T | null = null
let reportError: unknown = null
void waitForReport().then(
value => {
resolvedReport = valueView on GitHub (pinned to ac49b09b7d)
Solutions
- Read lastPhase in the message: "loading" = page never finished setup (check page console for import/runtime errors); "measuring" = measurement loop stalled (font/canvas issue); "posting" = transport problem (hash overflow — switch to the POST path).
- Raise --timeout (corpus-check) / timeoutMs arg; a corpus sweep over many widths legitimately needs more than the 60s default.
- If the report is large, switch the calling script from loadHashReport to loadPostedReport (startPostedReportServer + POST sidechannel) to bypass hash length limits.
- Verify the requestId woven into the URL matches the report.requestId the page writes — a mismatched id makes every poll look like "no report".
- Open the same URL in a headed browser and watch the console; loadHashReport swallows page-side errors by design, so the page is the source of truth.
Example fix
// before
const report = await loadHashReport<CorpusReport>(session, url, requestId, browser, timeoutMs)
// after — for large reports, use the POST sidechannel instead of the hash
const reportServer = await startPostedReportServer<CorpusReport>(requestId)
try {
const posted = await loadPostedReport(
session,
urlWithReportEndpoint(url, reportServer.endpoint),
() => reportServer.waitForReport(null),
requestId,
browser,
timeoutMs,
)
} finally {
reportServer.close()
} Defensive patterns
Strategy: retry
Validate before calling
// Choose transport by payload shape BEFORE picking loadHashReport
function shouldUsePostTransport(corpusMeta: { rows?: number }, widths: number[]): boolean {
const approxRows = (corpusMeta.rows ?? 1) * widths.length
// Browsers cap URL/hash near 2k (Safari) to ~32k; anything big should POST
return approxRows > 50
} Try / catch
try {
return await loadHashReport(session, url, requestId, browser, timeoutMs)
} catch (error) {
if (error instanceof Error && error.message.includes('last phase: posting')) {
// Hash overflow is the most likely posting-phase failure — switch to the POST sidechannel
const server = await startPostedReportServer<T>(requestId)
try {
return await loadPostedReport(session, `${url}&reportEndpoint=${encodeURIComponent(server.endpoint)}`, () => server.waitForReport(null), requestId, browser, timeoutMs)
} finally {
server.close()
}
}
throw error
} Prevention
- Use the POST sidechannel (loadPostedReport) for any report that could exceed a few KB — hash transport silently truncates.
- Always weave a fresh requestId per measurement and assert it back; stale tabs echo old reports that fail the requestId match and look like a timeout.
- Set --timeout generously for corpus sweeps (the default 60s is tight for cold browsers); raise it rather than guessing.
- In dev, open the same URL in a headed browser and watch the console — loadHashReport intentionally swallows page errors.
When it happens
Trigger: loadHashReport polls up to ceil(timeoutMs/100) times. Each tick: readLocationUrl, readNavigationPhaseState to track lastPhase, readNavigationReportText to extract report JSON. Throw when no report.requestId === expectedRequestId ever matched. Concrete causes: the page threw before reaching the hash-write step (lastPhase stays "loading" or "measuring"); the report payload exceeded the browser's URL/hash length cap so it was truncated/never written (lastPhase "posting"); the requestId encoded in the URL did not match what the page echoes back; the tab was navigated away or closed by the user.
Common situations: An accuracy/corpus page that produces a large report (many rows) overflows the hash transport — use loadPostedReport (the POST sidechannel) instead; the page hit a runtime error mid-measurement (font not loaded, DOM missing) and never reached the report step; a stale browser tab from a prior run intercepted the navigation; Safari's stricter URL-length handling truncates the hash earlier than Chromium; --timeout too low for a slow Corpus on a cold browser.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Timed out waiting for posted report from ${browser} (last ph
- Timed out waiting for local port ${port}
- Timed out waiting for ${browser} automation lock
- ${navigate.message ?? navigate.error}
- Timed out waiting for local Bun server on port ${port}${path
AI-assisted analysis of chenglou/pretext@ac49b09b7d (2026-08-12).
Data as JSON: /api/errors/9bd0c34e4e83963f.
Report an issue: GitHub.