{"record":{"id":"ee49ecfd76ea1b08","repo":"santifer/career-ops","slug":"local-parser-returned-invalid-json","errorCode":null,"errorMessage":"local parser returned invalid JSON","messagePattern":"local parser returned invalid JSON","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/local-parser.mjs","lineNumber":177,"sourceCode":"  const parser = entry.parser || {};\n  const { command, args } = resolveInvocation(entry);\n  const timeout = Number(parser.timeout_ms || LOCAL_PARSER_TIMEOUT_MS);\n  const maxBuffer = Number(parser.max_buffer_bytes || LOCAL_PARSER_MAX_BUFFER_BYTES);\n\n  // cwd is pinned to the project root so a relative script arg resolves to the\n  // same file resolveInvocation() validated, regardless of the caller's cwd.\n  const { stdout } = await execFileAsync(command, args, {\n    cwd: PROJECT_ROOT,\n    timeout,\n    maxBuffer,\n    windowsHide: true,\n  });\n\n  let payload;\n  try {\n    payload = JSON.parse(stdout);\n  } catch {\n    throw new Error('local parser returned invalid JSON');\n  }\n\n  const rawJobs = Array.isArray(payload) ? payload : payload.jobs || payload.results;\n  if (!Array.isArray(rawJobs)) {\n    throw new Error('local parser JSON must be an array or contain jobs[]/results[]');\n  }\n\n  return rawJobs\n    .map(job => normalizeParserJob(job, entry))\n    .filter(Boolean);\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'local-parser',\n\n  detect(entry) {\n    if (!entry.parser?.command) return null;","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/local-parser.mjs#L159-L195","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (parser script)\nconsole.log('fetching jobs...');\nconsole.log(JSON.stringify(jobs));\n// after\nconsole.error('fetching jobs...');\nconsole.log(JSON.stringify(jobs));","handlingStrategy":"try-catch","validationCode":"const { stdout } = await execFileAsync(cmd, args, { cwd: ROOT });\nJSON.parse(stdout); // fail fast in a smoke test before wiring the entry into portals.yml","typeGuard":null,"tryCatchPattern":"try {\n  const jobs = await localParser.fetch(entry);\n} catch (e) {\n  if (String(e.message).includes('returned invalid JSON')) {\n    console.error(`${entry.name}: parser stdout is not JSON — move logs to stderr and emit one JSON document`);\n    return [];\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["json","subprocess","parser"],"backgroundTag":"json-parse-error","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"}