santifer/career-ops · error · Error
local parser returned invalid JSON
Error message
local parser returned invalid JSON
What it means
runLocalParser() executes the configured parser via execFile and expects its stdout to be valid JSON. If JSON.parse(stdout) fails — the parser printed diagnostics, HTML, partial output, or mixed logging into stdout — this error is thrown so the entry is treated as a failed fetch rather than silently returning no jobs.
Solutions
- Fix the parser script to emit exactly one JSON document (array or {jobs:[...]} / {results:[...]}) on stdout and send all logging to stderr.
- Run the script manually from the repo root with the same args and inspect its raw stdout: `python3 parsers/jobs.py <url> | cat`.
- Move progress/banner output to console.error / sys.stderr in the script.
- If the parser is third-party and noisy, wrap it: pipe stdout through a small in-repo script that extracts the JSON payload.
- Check timeout/maxBuffer settings — a killed or truncated run can leave partial stdout; raise parser.timeout_ms / max_buffer_bytes if output is large.
Example fix
// before (parser script)
console.log('fetching jobs...');
console.log(JSON.stringify(jobs));
// after
console.error('fetching jobs...');
console.log(JSON.stringify(jobs)); Defensive patterns
Strategy: try-catch
Validate before calling
const { stdout } = await execFileAsync(cmd, args, { cwd: ROOT });
JSON.parse(stdout); // fail fast in a smoke test before wiring the entry into portals.yml Try / catch
try {
const jobs = await localParser.fetch(entry);
} catch (e) {
if (String(e.message).includes('returned invalid JSON')) {
console.error(`${entry.name}: parser stdout is not JSON — move logs to stderr and emit one JSON document`);
return [];
}
throw e;
} Prevention
- Reserve stdout exclusively for the JSON payload; all logs go to stderr.
- Smoke-test each parser from the repo root with the exact configured args.
- Emit a single JSON document — never NDJSON or trailing text.
- Raise parser.timeout_ms / max_buffer_bytes for large boards so output is not truncated mid-document.
When it happens
Trigger: The spawned parser exits 0 but its stdout is not parseable JSON: it prints human-readable logs, prints a BOM/trailing garbage, writes results to stderr while stdout holds prose, or gets truncated by maxBuffer (which surfaces differently) / killed by timeout producing empty or partial output.
Common situations: Parser script using print/console.log for progress messages instead of stderr; script emitting NDJSON (one JSON per line) instead of one JSON document; dependency warning banners printed to stdout; script crashing after printing partial output on stdout.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- local parser JSON must be an array or contain…
- clone of @ failed
- collage: unrecognized response envelope (expected a…
- Could not parse existing candidates file at
- each entry needs a string "input" and "output"
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/ee49ecfd76ea1b08.
Report an issue: GitHub.
Appendix: source
Thrown at providers/local-parser.mjs:177
const parser = entry.parser || {};
const { command, args } = resolveInvocation(entry);
const timeout = Number(parser.timeout_ms || LOCAL_PARSER_TIMEOUT_MS);
const maxBuffer = Number(parser.max_buffer_bytes || LOCAL_PARSER_MAX_BUFFER_BYTES);
// cwd is pinned to the project root so a relative script arg resolves to the
// same file resolveInvocation() validated, regardless of the caller's cwd.
const { stdout } = await execFileAsync(command, args, {
cwd: PROJECT_ROOT,
timeout,
maxBuffer,
windowsHide: true,
});
let payload;
try {
payload = JSON.parse(stdout);
} catch {
throw new Error('local parser returned invalid JSON');
}
const rawJobs = Array.isArray(payload) ? payload : payload.jobs || payload.results;
if (!Array.isArray(rawJobs)) {
throw new Error('local parser JSON must be an array or contain jobs[]/results[]');
}
return rawJobs
.map(job => normalizeParserJob(job, entry))
.filter(Boolean);
}
/** @type {Provider} */
export default {
id: 'local-parser',
detect(entry) {
if (!entry.parser?.command) return null;View on GitHub (pinned to aac998c7ed)