{"record":{"id":"916d448f0d28a56b","repo":"santifer/career-ops","slug":"a16z-speedrun-talent-unexpected-api-response-on-p","errorCode":null,"errorMessage":"a16z-speedrun-talent: unexpected API response on page ${page} — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]","messagePattern":"a16z-speedrun-talent: unexpected API response on page (.+?) — expected (.+?), got keys: \\[(.+?)\\]","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/a16z-speedrun-talent.mjs","lineNumber":171,"sourceCode":"    assertFeedUrl(FEED_BASE);\n    const maxPages = resolveMaxPages(entry);\n    const q = resolveQuery(entry);\n    const fallbackCompany = entry?.name;\n    const out = [];\n\n    for (let page = 0; page < maxPages; page++) {\n      const params = new URLSearchParams({ page: String(page), source: 'career-ops' });\n      if (q) params.set('q', q);\n      const url = `${FEED_BASE}?${params}`;\n      // redirect:'error' prevents SSRF via server-side redirects.\n      // Retried on transient upstream failures (429/5xx/timeout): this board\n      // paginates into the hundreds of pages, so a single blip mid-sweep used\n      // to abort the whole provider and return NOTHING. Retries are bounded\n      // and, once exhausted, the error still propagates — a silent partial\n      // board would be worse than an loud empty one (#2506).\n      const json = await fetchJsonWithRetry(ctx, url, { redirect: 'error' });\n      if (!json || !Array.isArray(json.jobs)) {\n        throw new Error(\n          `a16z-speedrun-talent: unexpected API response on page ${page} — expected { jobs: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`,\n        );\n      }\n      for (const j of json.jobs) {\n        const normalized = normalizeSpeedrunJob(j, fallbackCompany);\n        if (normalized) out.push(normalized);\n      }\n      // Stop at the last page. Termination priority (#2547):\n      //   1. An empty page always ends iteration — nothing left to read, and\n      //      it bounds a feed that over-reports total_pages instead of burning\n      //      requests up to max_pages.\n      //   2. The feed's own total_pages when present. A page that comes back\n      //      49/50 because a listing was deleted between requests on a board\n      //      that rotates mid-sweep is NOT the last page; treating it as one\n      //      ended the sweep silently, at a clean exit code — the cap warning\n      //      below is gated on max_pages and is never reached from here.\n      //      A non-positive total_pages is not a statement about board size,\n      //      so it counts as absent: a feed reporting 0 alongside 50 real","sourceCodeStart":153,"sourceCodeEnd":189,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/a16z-speedrun-talent.mjs#L153-L189","documentation":"Thrown by the a16z-speedrun-talent provider's fetch when a page of its paginated feed returns JSON that lacks a `jobs` array. Each page is validated (json truthy and Array.isArray(json.jobs)) after a bounded retry via fetchJsonWithRetry — the retries cover transient blips mid-sweep because a single failure used to abort the whole provider and return nothing (#2506). Once retries are exhausted or the shape is still wrong, this error propagates: a silent partial board is considered worse than a loud failure. The message lists the actual top-level keys for diagnosis.","triggerScenarios":"fetchJsonWithRetry on a feed page URL returns null, an object without `jobs`, or `jobs` not an array — API schema change, error payload with 200 status, or last-page/overflow page returning a different body; retries (redirect:'error') already exhausted.","commonSituations":"The speedrun talent feed API changed its response shape; requesting pages beyond the feed's end; a WAF/CDN serving JSON error bodies; an outage returning {message:'...'} objects that survive retries.","solutions":["Fetch the page URL manually and compare the reported keys against the expected { jobs: [...] } shape.","Check whether the feed API migrated/changed schema and update normalizeSpeedrunJob/parsing accordingly.","Cap max_pages or stop at the first page whose job list is empty to avoid querying non-existent pages.","If a proxy/WAF rewrites responses, bypass it so the real feed JSON arrives."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"const json = await fetchJsonWithRetry(ctx, url, { redirect: 'error' });\nif (!json || !Array.isArray(json.jobs)) {\n  throw new Error(`speedrun feed shape changed on ${url}: keys=${Object.keys(json ?? {}).join(',')}`);\n}","typeGuard":"function isSpeedrunJobsPage(json) {\n  return typeof json === 'object' && json !== null && Array.isArray(json.jobs);\n}","tryCatchPattern":"try {\n  const jobs = await fetchSpeedrunFeedPage(page);\n} catch (e) {\n  if (String(e.message).includes('unexpected API response')) {\n    console.error(`Speedrun feed shape unexpected at page ${page}; schema may have changed`);\n    return partialResults; // decide loudly, never silently truncate\n  } else throw e;\n}","preventionTips":["Retry transient page failures with a bounded backoff before giving up.","Stop pagination when a page comes back empty rather than querying past the end.","Alert on schema changes: log the actual keys from the error message.","Verify the feed endpoint manually after provider/API updates."],"tags":["api","response-shape","pagination","retry","provider"],"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"}