santifer/career-ops · warning
[fetch] Playwright error
Error message
[fetch] Playwright error: ${e.message} — falling back to plain fetch. What it means
Fallback warning in openrouter-runner.mjs's fetchJobPage: the Playwright headless-browser extraction of a job page threw (launch failure, navigation timeout, page.evaluate error, or browser crashed), and the runner logs this message and falls through to a plain HTTP fetch of the same URL.
Solutions
- Install browser binaries: npx playwright install chromium
- Read err.message: timeout → increase goto timeout or use waitUntil:'domcontentloaded'; missing body → guard `document.body?.innerText`
- If Playwright is unavailable, ensure the plain-fetch fallback yields usable text and expect degraded extraction for JS-heavy pages
- For blocked pages, set a realistic user agent or run headed mode
Example fix
// before
const text = await page.evaluate(() => {
document.querySelectorAll('script,style,nav,footer,header').forEach(el => el.remove());
return (document.body?.innerText || document.body?.textContent || '').replace(/\s+/g, ' ').trim();
});
// after
const text = await page.evaluate(() => {
document.querySelectorAll('script,style,nav,footer,header').forEach(el => el.remove());
return ((document.body?.innerText ?? document.body?.textContent) || '').replace(/\s+/g, ' ').trim();
}); // plus try/catch around page.evaluate to return '' instead of throwing Defensive patterns
Strategy: fallback
Validate before calling
import { chromium } from 'playwright';
try { const b = await chromium.launch(); await b.close(); } catch { console.warn('Playwright browsers not installed; run: npx playwright install chromium'); } Type guard
function browserAvailable(mod) { try { mod.chromium.executablePath(); return true; } catch { return false; } } Try / catch
let text; try { text = await extractWithPlaywright(url); } catch (e) { console.warn(`[fetch] Playwright error: ${e.message} — falling back`); text = await plainFetch(url); } Prevention
- Run `npx playwright install` in every environment/container image
- Prefer waitUntil:'domcontentloaded' plus a settle delay over 'networkidle'
- Guard optional DOM access (document.body?.innerText) inside page.evaluate
- Set a realistic user agent for bot-protected job boards
When it happens
Trigger: page.goto times out (30s-ish, heavy JS pages), Playwright browser binary not installed, evaluate throws because document.body is null after removing script/nav elements, or browser.close fails after a crash.
Common situations: CI/container without `npx playwright install` browsers; JS-rendered SPA job boards that block headless Chrome; sites with anti-bot measures; slow network causing goto timeout.
Related errors
- [fetch] Playwright unavailable — falling back to plain…
- Playwright extraction failed or blocked, trying fallback…
- archive render skipped — no Playwright browser in env
- ⚠️ Browser cleanup failed
- --liveness requires Playwright with Chromium (run "npx…
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/a3ce124229a33401.
Report an issue: GitHub.
Appendix: source
Thrown at openrouter-runner.mjs:443
({ chromium } = await import('playwright'));
} catch {
console.warn('[fetch] Playwright unavailable — falling back to plain fetch.');
}
if (chromium) {
let browser;
try {
browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
await page.waitForTimeout(2000); // wait for SPA render
const text = await page.evaluate(() => {
document.querySelectorAll('script,style,nav,footer,header').forEach(el => el.remove());
return (document.body?.innerText || document.body?.textContent || '').replace(/\s+/g, ' ').trim();
});
return text.slice(0, 16_000);
} catch (e) {
console.warn(`[fetch] Playwright error: ${e.message} — falling back to plain fetch.`);
} finally {
if (browser) await browser.close().catch(() => {});
}
}
// Plain HTTP fallback
try {
const r = await fetch(url, {
headers: { 'User-Agent': DEFAULT_USER_AGENT }
});
if (!r.ok) throw new Error(`HTTP ${r.status} ${r.statusText}`);
const html = await r.text();
return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 16_000);
} catch (e) {
throw new Error(`Could not fetch job page: ${e.message}`);
}
}
View on GitHub (pinned to aac998c7ed)