{"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":"validation","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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/solidjobs.mjs#L49-L83","documentation":"After fetching, fetch verifies the JSON is an object with an Array field 'jobs'. If json is null/undefined/non-object or json.jobs is not an array, it throws and lists the actual top-level keys to aid diagnosis. This guards the .filter/.map pipeline downstream from a shape that does not match the documented { jobs: [...] } contract.","triggerScenarios":"The SolidJobs API returned an error envelope (e.g. { error: '...' }), an HTML maintenance page was parsed as non-JSON, the endpoint moved and now returns a different shape, or the network layer returned null. A redirect that redirect:'error' blocked surfaces here too if ctx.fetchJson resolves null on failure.","commonSituations":"API outage/maintenance window, a version bump that changed the response shape, an expired campaign query param, or hitting a rate-limit/CAPTCHA page.","solutions":["Open the careers_url in a browser and inspect the actual JSON — compare top-level keys to the error message","Confirm the division in the URL is still valid (it/engineering/marketing/sales/hr/logistics/finances/other)","Retry later if the API is in maintenance","If the contract changed, update the parser in providers/solidjobs.mjs to the new shape"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"/** @param {unknown} json @returns {json is { jobs: any[] }} */\nfunction isSolidJobsResponse(json) {\n  return !!json && typeof json === 'object' && Array.isArray(json.jobs);\n}","tryCatchPattern":"try {\n  const jobs = await provider.fetch(entry, ctx);\n} catch (err) {\n  if (/solidjobs: unexpected API response/.test(err.message)) {\n    console.warn(`${entry.name}: SolidJobs API shape changed or is down — skipping`);\n    return [];\n  }\n  throw err;\n}","preventionTips":["Treat API-shape errors as transient: log and skip rather than aborting a batch scan.","Monitor the SolidJobs API for contract changes after version bumps.","Confirm redirect:'error' isn't turning a redirect into a null body."],"tags":["api-contract","runtime","solidjobs","response-shape"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}