santifer/career-ops · warning

[fetch] Playwright unavailable — falling back to plain fetch

Error message

[fetch] Playwright unavailable — falling back to plain fetch.

What it means

fetchJobPage() in openrouter-runner.mjs prefers Playwright chromium to render job pages (SPAs need JS execution), after assertSafeRemoteUrl() blocks private/loopback hosts. The dynamic import('playwright') is wrapped in try/catch: if the package is not installed in this checkout, it warns and falls back to plain fetch, which often returns an empty JS shell for client-side-rendered ATS pages -- degrading the downstream evaluation input.

Source

Thrown at openrouter-runner.mjs:404

  try { u = new URL(url); } catch { throw new Error(`Invalid URL: ${url}`); }
  if (u.protocol !== 'https:' && u.protocol !== 'http:') {
    throw new Error(`Refusing non-HTTP(S) URL: ${url}`);
  }
  const host = u.hostname.toLowerCase();
  const blocked = host === 'localhost' || host === '::1' || host.endsWith('.local') ||
    /^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) ||
    /^169\.254\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
  if (blocked) throw new Error(`Refusing private/loopback host: ${host}`);
  return u;
}

async function fetchJobPage(url) {
  assertSafeRemoteUrl(url);
  let chromium;
  try {
    ({ 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(() => {});

View on GitHub (pinned to 60398d6549)

Solutions

  1. Install the dependency: npm install playwright && npx playwright install chromium in the repo root.
  2. Verify the import resolves: node -e "import('playwright').then(()=>console.log('ok'),e=>console.error(e.message))".
  3. If a fetch fallback already produced a contentless capture, re-run the evaluation for that URL after installing.

Example fix

# before
[fetch] Playwright unavailable — falling back to plain fetch.
# after
$ npm install playwright
$ npx playwright install chromium
$ node openrouter-runner.mjs eval https://example.com/jobs/1
Defensive patterns

Strategy: fallback

Validate before calling

// Detect the degraded mode up front and refuse URLs that need JS rendering
let playwrightOk = false;
try { await import('playwright'); playwrightOk = true; } catch {}
if (!playwrightOk && /lever\.co|greenhouse\.io\/job\/app/.test(url)) {
  throw new Error('SPA posting needs Playwright — npm i playwright && npx playwright install chromium');
}

Prevention

When it happens

Trigger: npm install run without playwright in dependencies; a stripped-down/global node_modules that lacks playwright; module resolution blocked by NODE_PATH or policy; playwright present but the import itself throws before browser launch.

Common situations: CI images omitting browser packages to save size; minimal local installs; container deploys where the fallback is acceptable only for server-rendered pages.

Related errors


AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20). Data as JSON: /api/errors/d99f6769cbd70a48. Report an issue: GitHub.