{"record":{"id":"3df3f9dcb12d8a07","repo":"santifer/career-ops","slug":"nofluffjobs-unexpected-api-response-expected","errorCode":null,"errorMessage":"nofluffjobs: unexpected API response — expected { postings: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]","messagePattern":"nofluffjobs: unexpected API response — expected (.+?), got keys: \\[(.+?)\\]","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/nofluffjobs.mjs","lineNumber":109,"sourceCode":"          applicationStatus: [],\n          province: [],\n          company: [],\n          id: [],\n          category: [],\n          keyword: [],\n          jobLanguage: [],\n          seniority: [],\n        },\n        pageSize: Number(entry.page_size || PAGE_SIZE),\n        withSalaryMatch: true,\n      };\n\n  return { url: apiUrl.href, body };\n}\n\nexport function parseNoFluffJobsResponse(json) {\n  if (!json || !Array.isArray(json.postings)) {\n    throw new Error(`nofluffjobs: unexpected API response — expected { postings: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);\n  }\n  return json.postings\n    .filter(posting => posting && typeof posting === 'object')\n    .map(posting => {\n      const title = String(posting.title || '').trim();\n      const company = String(posting.name || '').trim();\n      const slug = String(posting.url || posting.id || '').trim();\n      if (!title || !slug) return null;\n      return {\n        title,\n        url: `${JOB_BASE}${slug}`,\n        company,\n        location: normalizeLocation(posting),\n        postedAt: postedAtMillis(posting.posted),\n      };\n    })\n    .filter(Boolean);\n}","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/nofluffjobs.mjs#L91-L127","documentation":"parseNoFluffJobsResponse validates the shape of the JSON returned by the NoFluffJobs API. It requires an object with a 'postings' array; anything else (null body, an error object, an HTML error page parsed accidentally, a changed API contract) throws this error listing the top-level keys actually received.","triggerScenarios":"The API returned 200 with a JSON body lacking a postings array — e.g. { error: ... }, an auth-challenge payload, a rate-limit notice, a paginated envelope under a different key, or a null/empty body.","commonSituations":"NoFluffJobs changed or versioned their public API response shape; a proxy/WAF returned a JSON error page; the request succeeded but hit a maintenance/captcha endpoint; the caller fetched the wrong endpoint URL.","solutions":["Log the full response body and keys listed in the error to see what the API actually returned","Re-check the API endpoint URL and query parameters against NoFluffJobs' current public API","Check for rate limiting / WAF interception (429 or challenge pages) and add backoff or headers like a browser User-Agent","If the API contract changed, update parseNoFluffJobsResponse to the new field name/shape","Retry later if the API was temporarily degraded"],"exampleFix":"// before\nconst json = await res.json();\nreturn parseNoFluffJobsResponse(json);\n// after\nconst json = await res.json();\nif (!res.ok) throw new Error(`nofluffjobs: HTTP ${res.status}: ${JSON.stringify(json).slice(0, 200)}`);\nreturn parseNoFluffJobsResponse(json);","handlingStrategy":"try-catch","validationCode":"const res = await fetch(apiUrl);\nif (!res.ok) throw new Error(`HTTP ${res.status}`);\nconst ct = res.headers.get('content-type') || '';\nif (!ct.includes('application/json')) throw new Error(`non-JSON response: ${ct}`);","typeGuard":"function isPostingsPayload(v) {\n  return v !== null && typeof v === 'object' && !Array.isArray(v) && Array.isArray(v.postings);\n}","tryCatchPattern":"try {\n  return parseNoFluffJobsResponse(json);\n} catch (e) {\n  if (String(e.message).includes('unexpected API response')) {\n    log.error(`API shape changed or blocked; body keys: ${e.message}`);\n    return []; // degrade to empty scan instead of crashing the run\n  }\n  throw e;\n}","preventionTips":["Check res.ok and content-type before parsing JSON","Log raw response bodies on parse failure to spot WAF/captcha/error pages quickly","Pin and document the API endpoint version; re-verify the shape after vendor changes","Add exponential backoff for 429/5xx responses before giving up"],"tags":["api","response-parsing","schema","http"],"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"}