{"record":{"id":"a5e444d3235b3715","repo":"santifer/career-ops","slug":"provider-id-fetch-did-not-return-an-array","errorCode":null,"errorMessage":"${provider.id}: fetch() did not return an array","messagePattern":"(.+?): fetch\\(\\) did not return an array","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scan.mjs","lineNumber":2420,"sourceCode":"    let sourceName = provider.id === 'local-parser' ? 'local-parser' : `${provider.id}-api`;\n    try {\n      let jobs;\n      try {\n        jobs = await provider.fetch(company, ctx);\n      } catch (parserErr) {\n        if (provider.id !== 'local-parser') throw parserErr;\n        const fallback = resolveProvider(company, providers, { skipIds: ['local-parser'] });\n        if (!fallback || fallback.error) throw parserErr;\n        provider = fallback.provider;\n        sourceName = `${provider.id}-api`;\n        jobs = await provider.fetch(company, ctx);\n        errors.push({\n          company: company.name,\n          error: `local parser failed, used API fallback: ${parserErr.message}`,\n        });\n      }\n      if (!Array.isArray(jobs)) {\n        throw new Error(`${provider.id}: fetch() did not return an array`);\n      }\n      totalFound += jobs.length;\n      if (!company._isBoard && jobs.length === 0) {\n        emptyTargets.push(company.name);\n      }\n\n      for (const job of jobs) {\n        // Trust enrichment — runs before filters, never drops\n        const trustResult = trustValidator(job);\n        job.trustScore = trustResult.score;\n        job.trustFlags = trustResult.flags;\n        job.trustLevel = trustResult.level;\n\n        // Company blacklist (#1742) — the user's own do-not-apply decision,\n        // checked first: it's company-level, not a per-posting signal. Never\n        // silent: skips are counted and reported in the run summary, and\n        // --include-blacklisted lets the posting through annotated instead.\n        if (blacklist.size > 0) {","sourceCodeStart":2402,"sourceCodeEnd":2438,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/scan.mjs#L2402-L2438","documentation":"After a provider's fetch() resolves, scan.mjs verifies Array.isArray(jobs) and throws a plain Error if not. Each ATS provider (Greenhouse, Lever, Ashby, local parser, etc.) is contractually required to return an array of job objects; a non-array return is a provider-contract violation indicating a bug in the provider adapter or an unexpected API response shape.","triggerScenarios":"A provider returns null, undefined, an object, or a string instead of an array; an upstream ATS API changes its response envelope (e.g. wrapping jobs in {results: [...]}) and the adapter forwards it untransformed; a provider throws and a fallback returns a non-array.","commonSituations":"ATS vendor changes their API response format; a custom/local provider adapter that returns a single object for a one-job posting; a fetch that returns the raw Response object instead of parsed JSON.","solutions":["Inspect the provider adapter for the named provider.id — ensure it returns an array (even if empty).","If the upstream API envelope changed, normalize in the adapter: return payload.results || payload.jobs || [].","Guard before the throw: coerce jobs = Array.isArray(jobs) ? jobs : (jobs ? [jobs] : []).","Check the provider's error path — a swallowed exception returning undefined is a likely culprit."],"exampleFix":"// before (provider adapter)\nasync fetch(company, ctx) {\n  return await response.json(); // returns { jobs: [...] }\n}\n\n// after\nasync fetch(company, ctx) {\n  const payload = await response.json();\n  return Array.isArray(payload) ? payload : (payload.jobs || []);\n}","handlingStrategy":"type-guard","validationCode":"const jobs = await provider.fetch(company, ctx);\nif (!Array.isArray(jobs)) {\n  throw new TypeError(`${provider.id}: expected array, got ${typeof jobs}`);\n}","typeGuard":"function isJobsArray(result) {\n  return Array.isArray(result) && result.every(j => j && typeof j === 'object');\n}","tryCatchPattern":"let jobs = await provider.fetch(company, ctx);\nif (!Array.isArray(jobs)) {\n  console.warn(`${provider.id} returned non-array (${typeof jobs}); coercing`);\n  jobs = Array.isArray(jobs?.jobs) ? jobs.jobs : (jobs ? [jobs] : []);\n}","preventionTips":["Enforce Array.isArray in every provider adapter's return contract via tests.","Monitor upstream ATS API changelogs for response-envelope changes.","Add a contract test that asserts each provider returns an array for a fixture response."],"tags":["provider","contract-violation","scan","ats","type-check"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}