{"record":{"id":"ad7a128b586e07c0","repo":"santifer/career-ops","slug":"http-r-status-r-statustext","errorCode":null,"errorMessage":"HTTP ${r.status} ${r.statusText}","messagePattern":"HTTP \\$\\{r\\.status\\} \\$\\{r\\.statusText\\}","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"openrouter-runner.mjs","lineNumber":431,"sourceCode":"      await page.waitForTimeout(2000); // wait for SPA render\n      const text = await page.evaluate(() => {\n        document.querySelectorAll('script,style,nav,footer,header').forEach(el => el.remove());\n        return (document.body?.innerText || document.body?.textContent || '').replace(/\\s+/g, ' ').trim();\n      });\n      return text.slice(0, 16_000);\n    } catch (e) {\n      console.warn(`[fetch] Playwright error: ${e.message} — falling back to plain fetch.`);\n    } finally {\n      if (browser) await browser.close().catch(() => {});\n    }\n  }\n\n  // Plain HTTP fallback\n  try {\n    const r = await fetch(url, {\n      headers: { 'User-Agent': DEFAULT_USER_AGENT }\n    });\n    if (!r.ok) throw new Error(`HTTP ${r.status} ${r.statusText}`);\n    const html = await r.text();\n    return html.replace(/<[^>]+>/g, ' ').replace(/\\s+/g, ' ').trim().slice(0, 16_000);\n  } catch (e) {\n    throw new Error(`Could not fetch job page: ${e.message}`);\n  }\n}\n\n// ---------------------------------------------------------------------------\n// portals.yml parser — reads the canonical schema with js-yaml (same library and\n// field names as scan.mjs: `title_filter.positive/negative` + `tracked_companies`),\n// so it never drifts from the main scanner. The runner's no-CLI scan path covers\n// companies that expose a direct JSON `api:`; careers_url-only / Playwright /\n// search-query companies are handled by the full /career-ops scan pipeline.\n// `rawOverride` lets tests feed YAML text directly (see test-all.mjs drift guard).\n// ---------------------------------------------------------------------------\nfunction normKeywords(v) {\n  if (!Array.isArray(v)) return [];\n  return v.map(x => String(x ?? '').toLowerCase().trim()).filter(Boolean);","sourceCodeStart":413,"sourceCodeEnd":449,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/openrouter-runner.mjs#L413-L449","documentation":"Thrown by the plain-fetch fallback inside fetchJobPage() when the remote returns a non-2xx response (r.ok is false). It surfaces the raw HTTP status code and reason phrase so the caller can distinguish a dead URL (404) from a block (403) or a server fault (502/503).","triggerScenarios":"The job posting URL returns 404 (role closed/removed), 403 (bot/WAF block on the default User-Agent), 410 (gone), or 5xx (ATS outage); this fires only after Playwright is unavailable or also failed, since Playwright runs first.","commonSituations":"A Greenhouse/Lever/Ashby posting was taken down between when pipeline.md was seeded and when the runner fetched it; an ATS rate-limits the runner's IP; a Cloudflare-protected site returns 403 to the plain fetch UA; corporate proxy returns a 502.","solutions":["Retry once after a short delay — transient 5xx and rate-limit 429 are common.","Verify the URL is still live in a browser; if it 404s, mark the pipeline entry dead and drop it.","If you see 403/429, prefer the full /career-ops scan pipeline (which uses Playwright with a real browser UA) instead of the runner's plain fetch fallback.","Ensure playwright is installed so the Playwright-first path runs before this fallback."],"exampleFix":"// before: plain fetch hit a 403\nawait fetchJobPage(url);\n// after: ensure playwright is installed so the browser path is tried first\nnpm i playwright && npx playwright install chromium","handlingStrategy":"retry","validationCode":"null","typeGuard":"null","tryCatchPattern":"try {\n  const html = await fetchJobPage(url);\n} catch (e) {\n  if (/HTTP 4\\d\\d/.test(e.message)) {\n    // dead/block — mark pipeline entry and move on\n  } else if (/HTTP 5\\d\\d|ECONNRESET|ETIMEDOUT/.test(e.message)) {\n    // transient — retry with backoff\n  } else {\n    throw e;\n  }\n}","preventionTips":["Keep playwright installed so the browser path runs first and is more resilient to 403/WAF blocks.","Verify liveness in a browser before queueing a URL.","For rate-limited hosts, prefer the full /career-ops scan pipeline."],"tags":["network","http-status","fetch","openrouter-runner"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}