{"record":{"id":"98c70ef33c6b4bc2","repo":"santifer/career-ops","slug":"justjoin-unexpected-api-response-expected-dat","errorCode":null,"errorMessage":"justjoin: unexpected API response — expected { data: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]","messagePattern":"justjoin: unexpected API response — expected (.+?), got keys: \\[(.+?)\\]","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/justjoin.mjs","lineNumber":82,"sourceCode":"  const parsed = assertJustJoinUrl(apiUrl);\n  if (parsed.pathname !== '/api/candidate-api/offers') {\n    parsed.pathname = '/api/candidate-api/offers';\n    parsed.search = '';\n  }\n  parsed.searchParams.set('from', String(from));\n  parsed.searchParams.set('itemsCount', String(Number(entry.page_size || PAGE_SIZE)));\n  parsed.searchParams.set('cityRadius', String(Number(entry.city_radius || 30)));\n  parsed.searchParams.set('currency', String(entry.currency || 'pln').toLowerCase());\n  parsed.searchParams.set('orderBy', 'descending');\n  parsed.searchParams.set('sortBy', 'publishedAt');\n  parsed.searchParams.set('keywordType', 'any');\n  parsed.searchParams.set('isPromoted', 'true');\n  return parsed.href;\n}\n\nexport function parseJustJoinResponse(json) {\n  if (!json || !Array.isArray(json.data)) {\n    throw new Error(`justjoin: unexpected API response — expected { data: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);\n  }\n  return json.data\n    .filter(offer => offer && typeof offer === 'object')\n    .map(offer => {\n      const slug = String(offer.slug || '').trim();\n      const title = String(offer.title || '').trim();\n      if (!slug || !title) return null;\n      return {\n        title,\n        url: `${JOB_BASE}${slug}`,\n        company: String(offer.companyName || '').trim(),\n        location: normalizeLocation(offer),\n        postedAt: postedAtMillis(offer.publishedAt),\n      };\n    })\n    .filter(Boolean);\n}\n","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/justjoin.mjs#L64-L100","documentation":"Thrown by parseJustJoinResponse when the JSON body from the justjoin.it API does not contain a `data` array — i.e. the response shape is not { data: [...] }. The error message includes the actual top-level keys (or 'null' for a non-object body) to aid debugging. This is a contract check protecting the mapper that iterates json.data, guarding against API changes, error payloads, or HTML/error pages parsed as JSON.","triggerScenarios":"ctx.fetchJson returns: null/undefined (empty body), an object without `data` (e.g. { error: '...', message: '...' }), an array at top level, or a JSON body whose `data` is an object/string instead of an array. Triggered on the first page of fetch() at providers/justjoin.mjs:118, or by any direct call to the exported parseJustJoinResponse.","commonSituations":"JustJoin changes or versions its candidate API (response envelope renamed); a Cloudflare/WAF block returns an HTML error page or JSON error object; rate limiting returns { errors: [...] }; the endpoint was hit with wrong query params producing an error envelope; a proxy returns an empty 200 body.","solutions":["Log/inspect the actual response body and its keys shown in the message to see what the API returned","Check whether the API returned an error/rate-limit payload (status code, Cloudflare challenge) and retry with backoff","Update the provider's parsing to the current JustJoin API envelope if the shape legitimately changed","Verify the request URL — use the default API_BASE rather than a hand-built entry.api URL with wrong parameters"],"exampleFix":"// before — passing a raw response straight to the mapper\nconst json = await res.json();\nconst jobs = json.data.map(toJob);\n// after — guard the shape first\nconst json = await res.json();\nif (!json || !Array.isArray(json.data)) {\n  throw new Error(`justjoin: unexpected API response, keys: ${json ? Object.keys(json) : 'null'}`);\n}\nconst jobs = json.data.map(toJob);","handlingStrategy":"try-catch","validationCode":"function looksLikeOffersEnvelope(json) {\n  return Boolean(json) && typeof json === 'object' && Array.isArray(json.data);\n}\nif (!looksLikeOffersEnvelope(json)) throw new Error('unexpected justjoin response shape');","typeGuard":"const isOffersEnvelope = (j) => Boolean(j) && typeof j === 'object' && Array.isArray(j.data);","tryCatchPattern":"try {\n  const parsed = parseJustJoinResponse(json);\n} catch (e) {\n  if (e.message.includes('unexpected API response')) {\n    console.error('JustJoin API envelope changed or returned an error payload; keys were:',\n      json && typeof json === 'object' ? Object.keys(json) : json);\n    // inspect body, back off / retry, or update parser\n  } else throw e;\n}","preventionTips":["Log the raw response body (and HTTP status) whenever the shape check fails","Watch for JustJoin API announcements; pin/verify the envelope after provider updates","Treat a non-{data:[...]} body as a signal of WAF/rate-limit/error pages, not just a parser bug"],"tags":["api","response-shape","validation","network"],"backgroundTag":"unexpected-api-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"}