{"record":{"id":"5e125c374a638746","repo":"santifer/career-ops","slug":"msg","errorCode":null,"errorMessage":"${msg}","messagePattern":"\\$\\{msg\\}","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/src/lib/apply/session.ts","lineNumber":186,"sourceCode":"async function nudgeScroll(page: Page): Promise<void> {\n  for (let i = 1; i <= 3; i++) {\n    await page.evaluate((y) => window.scrollTo(0, y), i * 1200).catch(() => {});\n    await page.waitForTimeout(250);\n  }\n  await page.evaluate(() => window.scrollTo(0, 0)).catch(() => {});\n}\n\nexport async function openSession(url: string, cliId?: string, forceAgent?: boolean, noApplyBtn?: boolean): Promise<{ id: string; title: string; fields: ApplyField[]; shots: string[]; issues: ApplyIssue[]; needsDrive?: boolean }> {\n  prune();\n  if (globalThis.__coIdleTimer) clearTimeout(globalThis.__coIdleTimer); // someone's active\n  const browser = await headedBrowser();\n  const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });\n  context.setDefaultTimeout(8000); // no single action hangs the whole open/fill\n  const page = await context.newPage();\n  const abort = async (msg: string): Promise<never> => {\n    await context.close().catch(() => {});\n    if (SESSIONS.size === 0) scheduleIdleClose();\n    throw new Error(msg);\n  };\n  // Capture the real form as we read it → a \"behind the scenes\" progress strip\n  // that proves we genuinely opened + parsed THEIR form (not magic). The last\n  // shot doubles as a subtle blurred backdrop behind the clean proxy.\n  const shots: string[] = [];\n  const snap = async () => {\n    try {\n      const b = await page.screenshot({ type: \"jpeg\", quality: 42 });\n      shots.push(`data:image/jpeg;base64,${b.toString(\"base64\")}`);\n    } catch {\n      /* ignore */\n    }\n  };\n  // 1) Navigate (resilient) → cheapest hard-block check on the Response status.\n  const resp = await gotoResilient(page, url);\n  await snap(); // first paint\n  const sBlock = statusBlock(resp?.status(), resp ? resp.headers() : {});\n  if (sBlock) return abort(sBlock.message);","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/web/src/lib/apply/session.ts#L168-L204","documentation":"A generic, parameterized failure (`throw new Error(msg)`) raised inside the `abort` closure within `openSession`. `abort` is the single error path for any unrecoverable problem while opening a real application URL in the headed browser — it closes the context, schedules idle cleanup, and re-throws the supplied message. The `${msg}` placeholder means the actual cause is composed by the caller (navigation failure, page crash, timeout, missing form, etc.).","triggerScenarios":"Any call to `abort(msg)` inside `openSession`: the target URL is unreachable / DNS failure / returns non-HTML; `page.goto` exceeds the 8000ms `setDefaultTimeout`; the page closes or crashes mid-load; a selector the open-flow depends on (e.g. form detection) never appears and the code calls `abort('...')` with a descriptive reason. The screenshot snapshot loop itself swallows errors, so this is only explicit aborts.","commonSituations":"Pasting a JD URL behind authentication or a paywall; URL with a typo / wrong scheme; corporate proxy blocking the outbound navigation; extremely slow site exceeding 8s; ATS that redirects to a login page; anti-bot wall (Cloudflare) blocking the automated browser; ATS site redesign removing the expected form container.","solutions":["Read the full `msg` value in the thrown error — it is caller-supplied and names the concrete sub-failure; address that specific cause first.","Confirm the URL opens in a normal Chrome tab; if it needs login or throws a CAPTCHA, the apply feature cannot drive it — use manual `apply` instead.","If the site is just slow, raise `context.setDefaultTimeout` (currently 8000ms) or pass a longer `waitUntil` option to `page.goto`.","Check network/proxy: ensure outbound HTTPS works from the process environment (`curl -I <url>`).","If the site blocks automation, fall back to filling the form yourself with the prepared answers/PDF and use `handoffSession` only for the human review step."],"exampleFix":"// before\ncontext.setDefaultTimeout(8000);\nawait page.goto(url);\n// after — give slow ATS pages more room\ncontext.setDefaultTimeout(20000);\nawait page.goto(url, { waitUntil: 'domcontentloaded' });","handlingStrategy":"try-catch","validationCode":"// Validate the URL is reachable before handing it to openSession.\nasync function isReachable(url) {\n  try {\n    const res = await fetch(url, { method: 'GET', redirect: 'follow' });\n    const ct = res.headers.get('content-type') || '';\n    return res.ok && ct.includes('text/html');\n  } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const session = await openSession(url, cliId);\n} catch (e) {\n  // e.message is the caller-supplied abort reason; classify by content\n  if (/timeout|net::ERR|navigation/i.test(e.message)) {\n    showUser('Could not open the form at that URL. Check the link, login wall, or CAPTCHA.');\n  } else { showUser(e.message); }\n}","preventionTips":["Validate the URL (reachable + HTML) before invoking openSession.","Increase `context.setDefaultTimeout` if your target ATS is reliably slow.","Detect auth-wall / CAPTCHA responses upstream and route the user to manual apply.","Log the full `msg` so the specific abort reason is always visible."],"tags":["playwright","navigation","timeout","apply","runtime"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}