{"record":{"id":"3216a56453cd4270","repo":"santifer/career-ops","slug":"local-parser-careers-url-is-not-a-valid-url-va","errorCode":null,"errorMessage":"local-parser: careers_url is not a valid URL: ${value}","messagePattern":"local-parser: careers_url is not a valid URL: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/local-parser.mjs","lineNumber":29,"sourceCode":"\nconst LOCAL_PARSER_TIMEOUT_MS = 20_000;\nconst LOCAL_PARSER_MAX_BUFFER_BYTES = 2_000_000;\n\n// `parser.command` / `parser.script` come from portals.yml, which on a shared or\n// template config is not fully trusted. The command must be a known interpreter\n// or a file inside this project — never an arbitrary binary like `rm` or `curl`.\nconst PROJECT_ROOT = realpathSync(resolve(fileURLToPath(new URL('..', import.meta.url))));\nconst ALLOWED_INTERPRETERS = new Set(['python3', 'python', 'node', 'deno', 'bun', 'sh', 'bash']);\n\n// `{careers_url}` and `{company}` are interpolated into the parser's argv. Validate\n// them so an interpolated value can never be read as a CLI flag (argument injection).\nfunction safeCareersUrl(value) {\n  if (!value) return '';\n  let url;\n  try {\n    url = new URL(String(value));\n  } catch {\n    throw new Error(`local-parser: careers_url is not a valid URL: ${value}`);\n  }\n  if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n    throw new Error(`local-parser: careers_url must be http(s): ${value}`);\n  }\n  return url.href;\n}\n\nfunction safeCompany(value) {\n  if (!value) return '';\n  const name = String(value).trim();\n  // execFile passes args verbatim (no shell), so the only injection risk is a\n  // value that begins like a CLI flag.\n  if (name.startsWith('-')) {\n    throw new Error(`local-parser: company name cannot start with '-': ${value}`);\n  }\n  return name;\n}\n","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/local-parser.mjs#L11-L47","documentation":"safeCareersUrl runs entry.careers_url through new URL() before interpolating it into the parser's argv. If the value cannot be parsed as an absolute URL, this error fires. The guard exists because the value becomes a CLI argument — an unparseable or schemeless string could be misread by the parser process.","triggerScenarios":"entry.careers_url is empty-ish-but-present, relative ('/careers'), missing a scheme ('acme.com'), or contains characters the URL constructor rejects, AND the parser arg template contains {careers_url} (expansion only happens when the placeholder is used).","commonSituations":"A local-parser entry has a parser.args template using {careers_url} but careers_url is blank or relative; config was copy-pasted and the URL field left half-filled.","solutions":["Set careers_url to a full absolute URL, e.g. https://www.acme.com/careers.","If the parser does not actually need the URL, remove the {careers_url} placeholder from parser.args (safeCareersUrl is only called when the placeholder is present).","Trim whitespace/newlines from the config value."],"exampleFix":"# before\n- name: Acme\n  provider: local-parser\n  careers_url: acme.com/careers\n  parser: { command: python3, script: parsers/acme.py, args: ['{careers_url}'] }\n\n# after\n- name: Acme\n  provider: local-parser\n  careers_url: https://www.acme.com/careers\n  parser: { command: python3, script: parsers/acme.py, args: ['{careers_url}'] }","handlingStrategy":"validation","validationCode":"import { URL } from 'node:url';\nexport function isValidCareersUrl(value) {\n  if (!value) return false;\n  try { new URL(String(value)); return true; } catch { return false; }\n}\n// In the config loader, only validate when the parser template uses {careers_url}:\n// if (entry.parser?.args?.some(a => String(a).includes('{careers_url}')) && !isValidCareersUrl(entry.careers_url)) failConfig(...);","typeGuard":"/** @param {unknown} v */\nfunction isAbsoluteUrl(v) {\n  return typeof v === 'string' && v.length > 0 && (() => { try { new URL(v); return true; } catch { return false; } })();\n}","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (err.message.startsWith('local-parser:')) console.warn(`local-parser config error for ${entry.name}: ${err.message}`);\n  throw err;\n}","preventionTips":["Only include {careers_url} in parser.args when you actually pass a real URL.","Lint local-parser entries to ensure careers_url is a full absolute URL whenever the placeholder is used.","Trim whitespace from config strings at load time."],"tags":["url-validation","local-parser","argument-injection","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}