santifer/career-ops · error · Error

local-parser: careers_url must be http(s): ${value}

Error message

local-parser: careers_url must be http(s): ${value}

What it means

After safeCareersUrl parses the URL, it requires the protocol to be http: or https:. Any other scheme (file:, ftp:, data:, javascript:) is rejected. Unlike the cloud providers, http: is allowed here because an internal/intranet parser target may legitimately be plain HTTP.

Source

Thrown at providers/local-parser.mjs:32

// `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;
}

// Only validate a placeholder's value when the arg actually uses it — a fixed
// `parser.script` must not be rejected because some unrelated `{company}` value
// has punctuation it never sees.

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Use http:// or https:// for the careers_url value.
  2. If you intended to pass a local file path to the parser, do so via the parser script itself, not the {careers_url} placeholder.
  3. For intranet targets over plain HTTP, http:// is acceptable — just ensure the scheme is present.

Example fix

# before
careers_url: file:///opt/feeds/acme.xml

# after
careers_url: http://intranet.acme.local/careers
Defensive patterns

Strategy: validation

Validate before calling

import { URL } from 'node:url';
export function isHttpOrHttps(value) {
  try { const p = new URL(String(value)).protocol; return p === 'http:' || p === 'https:'; } catch { return false; }
}

Type guard

/** @param {string} url */
function isHttpScheme(url) {
  try { const p = new URL(url).protocol; return p === 'http:' || p === 'https:'; } catch { return false; }
}

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (err) {
  if (err.message.includes('must be http(s)')) console.warn(`fix scheme for ${entry.name}: ${err.message}`);
  throw err;
}

Prevention

When it happens

Trigger: entry.careers_url parses but uses a non-http(s) scheme — e.g. file:///path/to/feed, ftp://..., or a data: URL — and the parser template uses {careers_url}.

Common situations: A local file:// path was configured as careers_url; an internal tool URL was mis-typed with the wrong scheme; a data: URL was used for a test fixture.

Related errors


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