{"record":{"id":"9bd0c34e4e83963f","repo":"chenglou/pretext","slug":"timed-out-waiting-for-report-from-browser-last","errorCode":null,"errorMessage":"Timed out waiting for report from ${browser} (last phase: ${lastPhase})","messagePattern":"Timed out waiting for report from (.+?) \\(last phase: (.+?)\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/browser-automation.ts","lineNumber":664,"sourceCode":"      phase !== null &&\n      (phase.requestId === undefined || phase.requestId === expectedRequestId)\n    ) {\n      lastPhase = phase.phase\n    }\n    const reportJson = readNavigationReportText(currentUrl)\n    if (reportJson === '' || reportJson === 'null') continue\n\n    const report = JSON.parse(reportJson) as T\n    if (report.requestId === expectedRequestId) {\n      return report\n    }\n  }\n\n  if (lastPhase === null) {\n    lastPhase = await readLastNavigationPhase(session, expectedRequestId)\n  }\n  const observedUrl = formatObservedLocation(await session.readLocationUrl())\n  throw new Error(getTimeoutMessage(browser, 'report', lastPhase, observedUrl))\n}\n\nexport async function loadPostedReport<T extends { requestId?: string }>(\n  session: BrowserSession,\n  url: string,\n  waitForReport: () => Promise<T>,\n  expectedRequestId: string,\n  browser: BrowserKind,\n  timeoutMs = 60_000,\n): Promise<T> {\n  await session.navigate(url)\n\n  let resolvedReport: T | null = null\n  let reportError: unknown = null\n\n  void waitForReport().then(\n    value => {\n      resolvedReport = value","sourceCodeStart":646,"sourceCodeEnd":682,"githubUrl":"https://github.com/chenglou/pretext/blob/ac49b09b7d83ede19581fa94a8b892b07d309baf/scripts/browser-automation.ts#L646-L682","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst report = await loadHashReport<CorpusReport>(session, url, requestId, browser, timeoutMs)\n// after — for large reports, use the POST sidechannel instead of the hash\nconst reportServer = await startPostedReportServer<CorpusReport>(requestId)\ntry {\n  const posted = await loadPostedReport(\n    session,\n  urlWithReportEndpoint(url, reportServer.endpoint),\n    () => reportServer.waitForReport(null),\n    requestId,\n    browser,\n    timeoutMs,\n  )\n} finally {\n  reportServer.close()\n}","handlingStrategy":"retry","validationCode":"// Choose transport by payload shape BEFORE picking loadHashReport\nfunction shouldUsePostTransport(corpusMeta: { rows?: number }, widths: number[]): boolean {\n  const approxRows = (corpusMeta.rows ?? 1) * widths.length\n  // Browsers cap URL/hash near 2k (Safari) to ~32k; anything big should POST\n  return approxRows > 50\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await loadHashReport(session, url, requestId, browser, timeoutMs)\n} catch (error) {\n  if (error instanceof Error && error.message.includes('last phase: posting')) {\n    // Hash overflow is the most likely posting-phase failure — switch to the POST sidechannel\n    const server = await startPostedReportServer<T>(requestId)\n    try {\n      return await loadPostedReport(session, `${url}&reportEndpoint=${encodeURIComponent(server.endpoint)}`, () => server.waitForReport(null), requestId, browser, timeoutMs)\n    } finally {\n      server.close()\n    }\n  }\n  throw error\n}","preventionTips":["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."],"tags":["browser-automation","timeout","report","hash-transport","navigation"],"backgroundTag":null,"analyzedSha":"ac49b09b7d83ede19581fa94a8b892b07d309baf","analyzedAt":"2026-08-12T17:03:16.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}