santifer/career-ops · error · Error

nofluffjobs: unexpected API response — expected { postings:

Error message

nofluffjobs: unexpected API response — expected { postings: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]

What it means

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.

Source

Thrown at providers/nofluffjobs.mjs:109

          applicationStatus: [],
          province: [],
          company: [],
          id: [],
          category: [],
          keyword: [],
          jobLanguage: [],
          seniority: [],
        },
        pageSize: Number(entry.page_size || PAGE_SIZE),
        withSalaryMatch: true,
      };

  return { url: apiUrl.href, body };
}

export function parseNoFluffJobsResponse(json) {
  if (!json || !Array.isArray(json.postings)) {
    throw new Error(`nofluffjobs: unexpected API response — expected { postings: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
  }
  return json.postings
    .filter(posting => posting && typeof posting === 'object')
    .map(posting => {
      const title = String(posting.title || '').trim();
      const company = String(posting.name || '').trim();
      const slug = String(posting.url || posting.id || '').trim();
      if (!title || !slug) return null;
      return {
        title,
        url: `${JOB_BASE}${slug}`,
        company,
        location: normalizeLocation(posting),
        postedAt: postedAtMillis(posting.posted),
      };
    })
    .filter(Boolean);
}

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. 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.
  2. If the key was renamed, update parseNoFluffJobsResponse to read the new key (e.g. json.jobs instead of json.postings).
  3. 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.
  4. If buildRequest() sends an outdated body schema, update the criteria/keyword filter structure to match the current API.

Example fix

// before
export function parseNoFluffJobsResponse(json) {
  if (!json || !Array.isArray(json.postings)) {
    throw new Error(`nofluffjobs: unexpected API response ...`);
  }
  return json.postings.filter(...);
}

// after — tolerate a renamed key and give a clearer error
export function parseNoFluffJobsResponse(json) {
  const list = Array.isArray(json?.postings) ? json.postings
    : Array.isArray(json?.jobs) ? json.jobs
    : null;
  if (!list) {
    throw new Error(`nofluffjobs: unexpected API response — expected {postings:[]}, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`);
  }
  return list.filter(Boolean);
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Pre-flight: probe the API to verify it still returns {postings:[]}
const probe = await fetch('https://nofluffjobs.com/api/search/posting', {
  method: 'POST', body: JSON.stringify(criteria), headers: { 'content-type': 'application/json' }
}).then(r => r.json());
if (!probe || !Array.isArray(probe.postings)) {
  console.warn('nofluffjobs API shape changed — keys:', probe ? Object.keys(probe) : 'null');
}

Type guard

/** @param {unknown} json @returns {json is {postings: any[]}} */
function isNoFluffResponse(json) {
  return json != null
    && typeof json === 'object'
    && Array.isArray(json.postings);
}

// usage:
const json = await ctx.fetchJson(url, opts);
if (!isNoFluffResponse(json)) {
  return []; // or unwrap alternate key
}

Try / catch

try {
  await nofluffProvider.fetch(entry, ctx);
} catch (err) {
  if (String(err.message).startsWith('nofluffjobs: unexpected API response')) {
    console.error(`nofluffjobs API drift for ${entry.name}:`, err.message);
    continue; // skip, keep scanning other providers
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/3df3f9dcb12d8a07. Report an issue: GitHub.