{"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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/nofluffjobs.mjs#L91-L127","documentation":"Thrown by parseNoFluffJobsResponse() when the JSON response is falsy or lacks a 'postings' property that is an array. The NoFluffJobs search API (POST /api/search/posting) is contractually expected to return {postings: [...], totalCount: N}. Any deviation — null body, a different wrapper key, or postings as a non-array — is treated as an API contract break.","triggerScenarios":"The POST to /api/search/posting returned HTTP 200 but the body is: (1) null or undefined; (2) an object without a 'postings' key (e.g. {jobs:[...]} after an API rename); (3) an error envelope like {errors:[...]} returned with status 200; (4) {postings: null} or {postings: {}} where postings isn't an array.","commonSituations":"NoFluffJobs ships an unannounced API change renaming 'postings' to 'jobs' or wrapping in a 'data' envelope. The API silently returns an error object with HTTP 200 (common in some API gateways). A malformed request body (wrong filter shape) causes the API to return an empty/error response object rather than the expected feed.","solutions":["Inspect the raw API response: curl -X POST https://nofluffjobs.com/api/search/posting -H 'content-type: application/json' -d '{...}' to see the actual shape.","If the key was renamed, update parseNoFluffJobsResponse to read the new key (e.g. json.jobs instead of json.postings).","If the response is an error envelope with HTTP 200, add status-code checking in the fetch loop or detect known error keys before shape validation.","If buildRequest() sends an outdated body schema, update the criteria/keyword filter structure to match the current API."],"exampleFix":"// before\nexport function parseNoFluffJobsResponse(json) {\n  if (!json || !Array.isArray(json.postings)) {\n    throw new Error(`nofluffjobs: unexpected API response ...`);\n  }\n  return json.postings.filter(...);\n}\n\n// after — tolerate a renamed key and give a clearer error\nexport function parseNoFluffJobsResponse(json) {\n  const list = Array.isArray(json?.postings) ? json.postings\n    : Array.isArray(json?.jobs) ? json.jobs\n    : null;\n  if (!list) {\n    throw new Error(`nofluffjobs: unexpected API response — expected {postings:[]}, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);\n  }\n  return list.filter(Boolean);\n}","handlingStrategy":"type-guard","validationCode":"// Pre-flight: probe the API to verify it still returns {postings:[]}\nconst probe = await fetch('https://nofluffjobs.com/api/search/posting', {\n  method: 'POST', body: JSON.stringify(criteria), headers: { 'content-type': 'application/json' }\n}).then(r => r.json());\nif (!probe || !Array.isArray(probe.postings)) {\n  console.warn('nofluffjobs API shape changed — keys:', probe ? Object.keys(probe) : 'null');\n}","typeGuard":"/** @param {unknown} json @returns {json is {postings: any[]}} */\nfunction isNoFluffResponse(json) {\n  return json != null\n    && typeof json === 'object'\n    && Array.isArray(json.postings);\n}\n\n// usage:\nconst json = await ctx.fetchJson(url, opts);\nif (!isNoFluffResponse(json)) {\n  return []; // or unwrap alternate key\n}","tryCatchPattern":"try {\n  await nofluffProvider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('nofluffjobs: unexpected API response')) {\n    console.error(`nofluffjobs API drift for ${entry.name}:`, err.message);\n    continue; // skip, keep scanning other providers\n  }\n  throw err;\n}","preventionTips":["Wrap provider calls in try-catch to isolate API-drift failures from batch scans.","Log raw response keys when shape validation fails for diagnosis.","Monitor NoFluffJobs API changelog for breaking changes.","Update buildRequest body schema if filter parameters change."],"tags":["api-contract","response-validation","nofluffjobs","json"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}