santifer/career-ops · error · Error

justjoin: unexpected API response — expected

Error message

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

What it means

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.

Solutions

  1. Log/inspect the actual response body and its keys shown in the message to see what the API returned
  2. Check whether the API returned an error/rate-limit payload (status code, Cloudflare challenge) and retry with backoff
  3. Update the provider's parsing to the current JustJoin API envelope if the shape legitimately changed
  4. Verify the request URL — use the default API_BASE rather than a hand-built entry.api URL with wrong parameters

Example fix

// before — passing a raw response straight to the mapper
const json = await res.json();
const jobs = json.data.map(toJob);
// after — guard the shape first
const json = await res.json();
if (!json || !Array.isArray(json.data)) {
  throw new Error(`justjoin: unexpected API response, keys: ${json ? Object.keys(json) : 'null'}`);
}
const jobs = json.data.map(toJob);
Defensive patterns

Strategy: try-catch

Validate before calling

function looksLikeOffersEnvelope(json) {
  return Boolean(json) && typeof json === 'object' && Array.isArray(json.data);
}
if (!looksLikeOffersEnvelope(json)) throw new Error('unexpected justjoin response shape');

Type guard

const isOffersEnvelope = (j) => Boolean(j) && typeof j === 'object' && Array.isArray(j.data);

Try / catch

try {
  const parsed = parseJustJoinResponse(json);
} catch (e) {
  if (e.message.includes('unexpected API response')) {
    console.error('JustJoin API envelope changed or returned an error payload; keys were:',
      json && typeof json === 'object' ? Object.keys(json) : json);
    // inspect body, back off / retry, or update parser
  } else throw e;
}

Prevention

When it happens

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

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

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/98c70ef33c6b4bc2. Report an issue: GitHub.

Appendix: source

Thrown at providers/justjoin.mjs:82

  const parsed = assertJustJoinUrl(apiUrl);
  if (parsed.pathname !== '/api/candidate-api/offers') {
    parsed.pathname = '/api/candidate-api/offers';
    parsed.search = '';
  }
  parsed.searchParams.set('from', String(from));
  parsed.searchParams.set('itemsCount', String(Number(entry.page_size || PAGE_SIZE)));
  parsed.searchParams.set('cityRadius', String(Number(entry.city_radius || 30)));
  parsed.searchParams.set('currency', String(entry.currency || 'pln').toLowerCase());
  parsed.searchParams.set('orderBy', 'descending');
  parsed.searchParams.set('sortBy', 'publishedAt');
  parsed.searchParams.set('keywordType', 'any');
  parsed.searchParams.set('isPromoted', 'true');
  return parsed.href;
}

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

View on GitHub (pinned to aac998c7ed)