{"record":{"id":"b6ead8c3bc3192ad","repo":"santifer/career-ops","slug":"invalid-url-url","errorCode":null,"errorMessage":"Invalid URL: ${url}","messagePattern":"Invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"openrouter-runner.mjs","lineNumber":409,"sourceCode":"    ctx.profile,\n    '---',\n    'CV (Markdown):',\n    ctx.cv,\n    '---',\n    'OUTPUT LANGUAGE:',\n    languageInstruction,\n  ].filter(Boolean).join('\\n\\n');\n}\n\n// ---------------------------------------------------------------------------\n// Job page content fetcher (Playwright-first, plain fetch fallback)\n// ---------------------------------------------------------------------------\n// Reject unsafe fetch targets (SSRF defense-in-depth): http(s) only, never\n// loopback / link-local / private / cloud-metadata hosts. URLs come from the\n// user's own portals.yml / pipeline.md, but we still fail closed.\nfunction assertSafeRemoteUrl(url) {\n  let u;\n  try { u = new URL(url); } catch { throw new Error(`Invalid URL: ${url}`); }\n  if (u.protocol !== 'https:' && u.protocol !== 'http:') {\n    throw new Error(`Refusing non-HTTP(S) URL: ${url}`);\n  }\n  const host = u.hostname.toLowerCase();\n  const blocked = host === 'localhost' || host === '::1' || host.endsWith('.local') ||\n    /^127\\./.test(host) || /^10\\./.test(host) || /^192\\.168\\./.test(host) ||\n    /^169\\.254\\./.test(host) || /^172\\.(1[6-9]|2\\d|3[01])\\./.test(host);\n  if (blocked) throw new Error(`Refusing private/loopback host: ${host}`);\n  return u;\n}\n\nasync function fetchJobPage(url) {\n  assertSafeRemoteUrl(url);\n  let chromium;\n  try {\n    ({ chromium } = await import('playwright'));\n  } catch {\n    console.warn('[fetch] Playwright unavailable — falling back to plain fetch.');","sourceCodeStart":391,"sourceCodeEnd":427,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/openrouter-runner.mjs#L391-L427","documentation":"assertSafeRemoteUrl is an SSRF defense-in-depth gate run before fetching any job-page URL. When the input cannot be parsed by the URL constructor at all (malformed string, missing scheme, spaces, unencoded characters), it throws 'Invalid URL: <url>'. This is a fail-closed check: the library refuses to guess or repair URLs coming from user-maintained portals.yml / pipeline.md files.","triggerScenarios":"fetchJobPage (or any caller of assertSafeRemoteUrl) is given a string that new URL() rejects — e.g. an empty string, a bare hostname without a scheme ('example.com/jobs/123'), a URL with unencoded spaces or invalid characters, or a truncated/garbled entry read from portals.yml or pipeline.md.","commonSituations":"A hand-edited portals.yml careers_url missing 'https://', a pipeline.md line that wrapped the URL across lines or captured trailing markdown, a copy-paste dropping the scheme, or programmatic concatenation producing 'url:https://...' style prefixes.","solutions":["Fix the source entry so it is a fully qualified absolute URL including the scheme (https://...)","Trim whitespace, quotes, and markdown artifacts from the value in portals.yml / pipeline.md","Validate the string with new URL() in a scratch script before putting it back into the config","If the value legitimately has no scheme, add 'https://' explicitly rather than relying on defaults"],"exampleFix":"// before (portals.yml / pipeline.md entry)\ncareers_url: acme.com/careers\n\n// after\ncareers_url: https://acme.com/careers","handlingStrategy":"validation","validationCode":"function isValidHttpUrl(s) {\n  try {\n    const u = new URL(s);\n    return u.protocol === 'https:' || u.protocol === 'http:';\n  } catch { return false; }\n}\n// call before fetchJobPage: if (!isValidHttpUrl(entry.url)) skip/fix entry;","typeGuard":"function isUrlString(v) {\n  return typeof v === 'string' && v.length > 0 && new URL(v) instanceof URL ? v : null;\n}","tryCatchPattern":"let html;\ntry {\n  html = await fetchJobPage(url);\n} catch (e) {\n  if (e.message.startsWith('Invalid URL:')) {\n    console.error(`Fix config entry, not a valid URL: ${e.message}`);\n    return null; // skip entry, flag portals.yml/pipeline.md for manual fix\n  }\n  throw e;\n}","preventionTips":["Always write fully qualified URLs (scheme included) in portals.yml and pipeline.md","Lint config entries with new URL() in CI so malformed URLs never reach runtime","Trim whitespace/quotes/markdown when pasting URLs from emails or docs","Never build URLs by naive string concatenation — use the URL constructor"],"tags":["url","validation","ssrf","config"],"backgroundTag":"invalid-url-format","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}