{"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":"validation","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/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/local-parser.mjs#L11-L47","documentation":"safeCareersUrl() validates the {careers_url} placeholder before it is interpolated into a local parser's argv. If the value is empty-ish but truthy-unparseable — i.e. String(value) cannot be parsed by `new URL()` — it throws this error rather than passing a malformed URL to a subprocess.","triggerScenarios":"A portals.yml entry with parser args containing `{careers_url}` whose entry.careers_url is a malformed string (missing scheme, spaces, garbage) — `new URL(value)` throws and this error propagates out of expandParserArg during resolveInvocation/buildParserArgs.","commonSituations":"Careers URL entered without scheme ('acme.com/careers'); URL with unescaped spaces or stray characters; a copy-paste that included surrounding markdown or quotes; an entry where careers_url holds a relative path instead of an absolute URL.","solutions":["Correct the entry's careers_url in portals.yml to a fully qualified URL including the scheme, e.g. https://acme.com/careers.","URL-encode special characters (spaces, non-ASCII) in the configured value.","If the URL is optional for this parser, remove `{careers_url}` from the parser args so the validator is never invoked.","Note: an empty/falsy careers_url returns '' silently — this error only fires for non-empty unparseable values, so check for whitespace-only strings too (they fail new URL())."],"exampleFix":"// before (portals.yml)\ncareers_url: acme.com/jobs\nargs: [\"parser.py\", \"{careers_url}\"]\n// after\ncareers_url: https://acme.com/jobs\nargs: [\"parser.py\", \"{careers_url}\"]","handlingStrategy":"validation","validationCode":"let u;\ntry { u = new URL(entry.careers_url); } catch { throw new Error(`entry ${entry.name}: careers_url must be absolute, e.g. https://...`); }","typeGuard":null,"tryCatchPattern":"try {\n  await localParser.fetch(entry);\n} catch (e) {\n  if (String(e.message).includes('careers_url is not a valid URL')) {\n    console.error(`Config error in ${entry.name}: fix careers_url to an absolute http(s) URL`);\n    return [];\n  }\n  throw e;\n}","preventionTips":["Always store careers_url fully qualified with scheme in portals.yml.","Run a one-off config lint that new URL()s every careers_url before scans.","Avoid copy-pasting URLs with trailing punctuation or markdown.","Keep placeholders ({careers_url}) only in args of parsers that actually need them."],"tags":["validation","url","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"}