santifer/career-ops · error · Error

local-parser: careers_url is not a valid URL: ${value}

Error message

local-parser: careers_url is not a valid URL: ${value}

What it means

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.

Source

Thrown at providers/local-parser.mjs:29

const LOCAL_PARSER_TIMEOUT_MS = 20_000;
const LOCAL_PARSER_MAX_BUFFER_BYTES = 2_000_000;

// `parser.command` / `parser.script` come from portals.yml, which on a shared or
// template config is not fully trusted. The command must be a known interpreter
// or a file inside this project — never an arbitrary binary like `rm` or `curl`.
const PROJECT_ROOT = realpathSync(resolve(fileURLToPath(new URL('..', import.meta.url))));
const ALLOWED_INTERPRETERS = new Set(['python3', 'python', 'node', 'deno', 'bun', 'sh', 'bash']);

// `{careers_url}` and `{company}` are interpolated into the parser's argv. Validate
// them so an interpolated value can never be read as a CLI flag (argument injection).
function safeCareersUrl(value) {
  if (!value) return '';
  let url;
  try {
    url = new URL(String(value));
  } catch {
    throw new Error(`local-parser: careers_url is not a valid URL: ${value}`);
  }
  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
    throw new Error(`local-parser: careers_url must be http(s): ${value}`);
  }
  return url.href;
}

function safeCompany(value) {
  if (!value) return '';
  const name = String(value).trim();
  // execFile passes args verbatim (no shell), so the only injection risk is a
  // value that begins like a CLI flag.
  if (name.startsWith('-')) {
    throw new Error(`local-parser: company name cannot start with '-': ${value}`);
  }
  return name;
}

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Set careers_url to a full absolute URL, e.g. https://www.acme.com/careers.
  2. 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).
  3. Trim whitespace/newlines from the config value.

Example fix

# before
- name: Acme
  provider: local-parser
  careers_url: acme.com/careers
  parser: { command: python3, script: parsers/acme.py, args: ['{careers_url}'] }

# after
- name: Acme
  provider: local-parser
  careers_url: https://www.acme.com/careers
  parser: { command: python3, script: parsers/acme.py, args: ['{careers_url}'] }
Defensive patterns

Strategy: validation

Validate before calling

import { URL } from 'node:url';
export function isValidCareersUrl(value) {
  if (!value) return false;
  try { new URL(String(value)); return true; } catch { return false; }
}
// In the config loader, only validate when the parser template uses {careers_url}:
// if (entry.parser?.args?.some(a => String(a).includes('{careers_url}')) && !isValidCareersUrl(entry.careers_url)) failConfig(...);

Type guard

/** @param {unknown} v */
function isAbsoluteUrl(v) {
  return typeof v === 'string' && v.length > 0 && (() => { try { new URL(v); return true; } catch { return false; } })();
}

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (err) {
  if (err.message.startsWith('local-parser:')) console.warn(`local-parser config error for ${entry.name}: ${err.message}`);
  throw err;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/3216a56453cd4270. Report an issue: GitHub.