{"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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/justjoin.mjs#L64-L100","documentation":"Thrown by parseJustJoinResponse() when the parsed JSON from the justjoin.it API is null/falsy or lacks a top-level data array. The provider expects the response shape { data: [...offers], meta: {...} }; a different shape (e.g. a bare array, an error envelope, a { results: [...] } wrapper) means the API contract changed or a non-JSON-success body was parsed, and the provider fails rather than silently emitting zero jobs. The error includes the actual top-level keys to aid diagnosis.","triggerScenarios":"justjoin.it changed its API response envelope (renamed data to results, wrapped it, or versioned the shape); the API returned an error object (e.g. { error: '...' }) with a 200 status; a gateway/proxy rewrote the response; an HTML error page was JSON-parsed into an unexpected object.","commonSituations":"An API version bump on justjoin.it; rate-limiting that returns a JSON error body instead of the offers list; a transient gateway response with a different envelope.","solutions":["Inspect the actual response body (the error logs the top-level keys) and compare against the expected { data: [...] } shape.","If justjoin.it renamed the envelope, update parseJustJoinResponse to read the new key.","If the body is an error envelope, address the upstream cause (rate limit, auth, region) before retrying."],"exampleFix":"// before — expects data, API now returns offers\nexport function parseJustJoinResponse(json) {\n  if (!json || !Array.isArray(json.data)) throw new Error(...);\n  return json.data.map(...);\n}\n\n// after — accept renamed envelope\nexport function parseJustJoinResponse(json) {\n  const list = json?.data ?? json?.offers ?? json?.results;\n  if (!Array.isArray(list)) throw new Error(`justjoin: unexpected API response — got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);\n  return list.map(...);\n}","handlingStrategy":"type-guard","validationCode":"function justJoinResponseOk(json) {\n  return Boolean(json && Array.isArray(json.data));\n}","typeGuard":"/** @param {any} json @returns {boolean} */\nfunction isJustJoinOffersEnvelope(json) {\n  return Boolean(json && Array.isArray(json.data));\n}","tryCatchPattern":"try {\n  parsed = parseJustJoinResponse(json);\n} catch (err) {\n  if (/unexpected API response/.test(err.message)) {\n    console.error(`justjoin API contract changed — keys: ${Object.keys(json || {}).join(', ')}`);\n  }\n  throw err;\n}","preventionTips":["Pin a fixture of the expected { data: [...] } response in tests so an envelope change breaks CI.","Log the top-level keys whenever the guard trips to accelerate diagnosis.","Distinguish a renamed envelope (structure change) from an error body (upstream fault) before retrying."],"tags":["justjoin","api-contract","response-parsing","structure-change"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}