{"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":386,"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":368,"sourceCodeEnd":404,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/openrouter-runner.mjs#L368-L404","documentation":"Thrown by assertSafeRemoteUrl() in openrouter-runner.mjs when new URL(url) throws — i.e. the string is not a parseable URL (missing protocol, illegal characters, malformed scheme). This is the first guard in the SSRF defense-in-depth chain (before the protocol and private-host checks), failing closed on any URL the platform cannot parse. URLs here come from the user's portals.yml / pipeline.md job links.","triggerScenarios":"Passing a string that is not a valid URL: 'company.com/jobs' (no scheme), 'ftp://...' (handled by the next guard, not this one — this is purely parse failure), strings with spaces/control chars, bare paths like '/jobs/123', or undefined/null coerced to string.","commonSituations":"Pipeline entry missing the https:// scheme; a copy-paste that dropped the protocol; a malformed portal URL in portals.yml; an empty or whitespace URL field; a JD capture whose URL got truncated.","solutions":["Ensure the URL has an explicit scheme: prepend 'https://' if missing — 'https://company.com/jobs/123'.","Validate the URL field in portals.yml/pipeline.md is complete and not truncated.","Strip stray whitespace/control characters before passing.","If the field can be empty, guard for emptiness before calling assertSafeRemoteUrl."],"exampleFix":"// before\nassertSafeRemoteUrl('company.com/jobs/123');\n// throws: Invalid URL: company.com/jobs/123\n\n// after\nassertSafeRemoteUrl('https://company.com/jobs/123');\n// or normalize first\nconst u = raw.startsWith('http') ? raw : `https://${raw}`;","handlingStrategy":"validation","validationCode":"function normalizeUrl(raw) {\n  if (!raw || typeof raw !== 'string') return null;\n  const s = raw.trim();\n  if (!s) return null;\n  const withScheme = /^https?:\\/\\//i.test(s) ? s : `https://${s}`;\n  try { return new URL(withScheme); } catch { return null; }\n}\nconst u = normalizeUrl(input);\nif (!u) skip('Invalid URL — cannot parse');","typeGuard":"/** True if the value parses as a URL new URL() accepts. */\nfunction isParseableUrl(v) {\n  if (!v || typeof v !== 'string') return false;\n  try { new URL(v); return true; } catch { return false; }\n}","tryCatchPattern":"try {\n  assertSafeRemoteUrl(url);\n} catch (e) {\n  if (e.message.startsWith('Invalid URL:')) {\n    // normalize: prepend scheme, then retry once\n    const fixed = /^https?:\\/\\//i.test(url) ? url : `https://${url}`;\n    assertSafeRemoteUrl(fixed);\n  } else throw e;\n}","preventionTips":["Always store full https:// URLs in portals.yml/pipeline.md.","Run a URL-format lint over portal entries rejecting schemeless values.","Normalize schemeless inputs by prepending https:// before calling assertSafeRemoteUrl.","Guard empty/whitespace URL fields upstream."],"tags":["validation","url","ssrf","configuration","network"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}