{"record":{"id":"f3a70f6a8c0ce409","repo":"santifer/career-ops","slug":"solidjobs-unexpected-api-response-expected-jo","errorCode":null,"errorMessage":"solidjobs: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]","messagePattern":"solidjobs: unexpected API response — expected (.+?), got keys: \\[(.+?)\\]","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/solidjobs.mjs","lineNumber":67,"sourceCode":"        return { url };\n    } catch {}\n    return null;\n  },\n  \n  /**\n   * Fetches and normalizes job offers from the SolidJobs public API.\n   * * @param {{ careers_url?: string, name: string }} entry - The configuration entry being processed.\n   * @param {{ fetchJson: (url: string, opts?: { redirect?: 'error'|'follow'|'manual' }) => Promise<any> }} ctx - HTTP context.\n   * @returns {Promise<Array<{title: string, url: string, company: string, location: string}>>} Array of parsed job offers.\n   */\n  async fetch(entry, ctx) {\n    const url = entry.careers_url;\n    if (!url) throw new Error('solidjobs: careers_url required');\n    assertUrl(url);\n    // redirect:'error' prevents SSRF via server-side redirects\n    const json = await ctx.fetchJson(url, { redirect: 'error' });\n    if (!json || !Array.isArray(json.jobs)) {\n      throw new Error(`solidjobs: unexpected API response — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);\n    }\n\n    /** @type {Array<{ title?: string, url?: string, company?: string, locations?: string | string[] }>} */\n    const jobs = json.jobs;\n\n    return jobs\n      .filter(j => j && typeof j === 'object' && typeof j.url === 'string' && j.url.trim() !== '')\n      .map(j => ({\n        title: j.title || '',\n        url: /** @type {string} */ (j.url || '').trim(),\n        company: j.company || entry.name,\n        location: Array.isArray(j.locations) ? j.locations.join(', ') : (typeof j.locations === 'string' ? j.locations : ''),\n      }));\n  },\n};\n","sourceCodeStart":49,"sourceCodeEnd":83,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/solidjobs.mjs#L49-L83","documentation":"After fetching, the provider asserts the response is a JSON object with a jobs array (expected shape { jobs: [...] }). If the JSON is null, not an object, or lacks an array-valued jobs key, it throws, embedding the actual top-level keys (or 'null') to aid debugging. This catches API drift, HTML error pages parsed as junk, and proxy/auth walls.","triggerScenarios":"solid.jobs changes its API contract; a Cloudflare/auth wall returns JSON like { error: 'forbidden' }; fetchJson returns null on an empty body; an HTML error page fails JSON parsing upstream and json arrives null.","commonSituations":"Endpoint works but a newer API version returns { data: [...] } or { offers: [...] }; rate limiting returning a JSON error object; misconfigured careers_url hitting a different endpoint that returns valid JSON of the wrong shape.","solutions":["Log the printed key list to see what the endpoint actually returned and adapt","Confirm the careers_url points at the offers API (not another solidjobs endpoint)","Check whether solid.jobs changed its public API contract and update the provider's parsing","Retry later if it is a transient rate-limit/WAF response"],"exampleFix":"// endpoint drifted\n// before: { jobs: [...] }\n// after: { data: [...] } — update provider to read json.data || json.jobs\nconst jobs = json.jobs ?? json.data;","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"function hasExpectedShape(json) {\n  return !!json && typeof json === 'object' && Array.isArray(json.jobs);\n}","tryCatchPattern":"try {\n  offers = await provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.includes('unexpected API response')) {\n    console.error(`${entry.name}: solidjobs API shape changed or was blocked — inspect keys in message`);\n    return [];\n  }\n  throw e;\n}","preventionTips":["Read the key list in the message — it tells you exactly what the endpoint returned","Pin/bookmark the API contract and watch for solidjobs API changes","Check for WAF/rate-limit responses when failures cluster in time","Write the provider's expected schema into portals.yml comments for future editors"],"tags":["api","schema-validation","unexpected-response"],"backgroundTag":"unexpected-response-shape","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"}