{"record":{"id":"1e9ac712c7b2100e","repo":"santifer/career-ops","slug":"joinup-entry-name-failed-to-parse-next-data","errorCode":null,"errorMessage":"joinup: ${entry.name} failed to parse __NEXT_DATA__ — ${err.message}","messagePattern":"joinup: (.+?) failed to parse __NEXT_DATA__ — (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/joinup.mjs","lineNumber":59,"sourceCode":"    try { host = new URL(entry.careers_url || '').hostname; } catch { return null; }\n    return /(^|\\.)joinup\\.ch$/i.test(host) ? { url: BROWSE_URL } : null;\n  },\n\n  async fetch(entry, ctx) {\n    // redirect:'error' — BROWSE_URL is pinned to joinup.ch (https); a 3xx must\n    // not be followed to a private/metadata IP (matches every other provider).\n    const html = await ctx.fetchText(BROWSE_URL, { redirect: 'error' });\n    const m = html.match(/<script id=\"__NEXT_DATA__\"[^>]*>([\\s\\S]*?)<\\/script>/);\n    // Fail closed: a missing/unparseable __NEXT_DATA__ is a scraper break, not an\n    // empty board — throw so the scan logs it instead of silently reporting zero.\n    if (!m) throw new Error(`joinup: ${entry.name} page is missing __NEXT_DATA__ (structure changed?)`);\n    let hits = [];\n    try {\n      const data = JSON.parse(m[1]);\n      const ir = data?.props?.pageProps?.serverState?.initialResults?.jobs?.results;\n      hits = Array.isArray(ir) && ir[0]?.hits ? ir[0].hits : [];\n    } catch (err) {\n      throw new Error(`joinup: ${entry.name} failed to parse __NEXT_DATA__ — ${err.message}`);\n    }\n    return hits\n      .filter(h => h && h.slug && (h.title || h.headline))\n      .map(h => ({\n        title: h.title || h.headline || '',\n        url: `https://joinup.ch/job/${h.slug}`,\n        company: h.startup || entry.name || '',\n        location: typeof h.location === 'string' ? h.location\n          : (h.location?.name || h.location?.city || ''),\n        postedAt: toEpochMs(h.created),\n      }));\n  },\n};\n","sourceCodeStart":41,"sourceCodeEnd":73,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/joinup.mjs#L41-L73","documentation":"Thrown by joinup.fetch() when the __NEXT_DATA__ script tag was found (regex matched) but JSON.parse() of its contents threw, OR the parsed object's deep path (props.pageProps.serverState.initialResults.jobs.results) did not yield a usable hits array. The error message chains the underlying parse/access error so the root cause is visible.","triggerScenarios":"The __NEXT_DATA__ script contains malformed JSON (truncated, encoded, or injected with non-JSON content); the JSON parses but the nested serverState/initialResults structure differs from what the provider expects (keys renamed or moved); results is present but the inner hits array is absent so the fallback yields [].","commonSituations":"joinup.ch changed its server-state nesting (renamed initialResults or restructured results); a partial page render truncated the script content; a different page type was served that embeds a valid but differently-shaped __NEXT_DATA__.","solutions":["Capture the raw __NEXT_DATA__ content for the failing entry and validate it with a JSON linter.","If the structure changed, update the data-access path (props.pageProps.serverState.initialResults.jobs.results) to the new nesting.","If the JSON is genuinely truncated, investigate whether a content-encoding or transfer issue is cutting the response."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"function joinupPayloadOk(html) {\n  const m = html.match(/<script id=\"__NEXT_DATA__\"[^>]*>([\\s\\S]*?)<\\/script>/);\n  if (!m) return false;\n  try {\n    const ir = JSON.parse(m[1])?.props?.pageProps?.serverState?.initialResults?.jobs?.results;\n    return Array.isArray(ir) && (!ir[0] || Array.isArray(ir[0].hits));\n  } catch { return false; }\n}","typeGuard":"/** @param {any} data @returns {boolean} */\nfunction joinupDataHasHits(data) {\n  const ir = data?.props?.pageProps?.serverState?.initialResults?.jobs?.results;\n  return Array.isArray(ir) && (!ir[0] || Array.isArray(ir[0].hits));\n}","tryCatchPattern":"try {\n  jobs = await provider.fetch(entry, ctx);\n} catch (err) {\n  if (/failed to parse __NEXT_DATA__/.test(err.message)) {\n    console.error(`joinup ${entry.name}: JSON parse failed — ${err.message}`);\n  }\n  throw err;\n}","preventionTips":["Validate the nested serverState path exists before depending on it; log the actual keys on failure.","Keep a fixture payload in tests so a joinup.ch nesting change breaks CI, not production scans.","Distinguish JSON.parse failures (truncated/encoded) from access failures (renamed keys) in logs."],"tags":["joinup","nextjs","json-parse","structure-change","scraping"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}