{"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":170,"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: a short page, or past the reported total_pages.\n      if (json.jobs.length < PER_PAGE) break;\n      if (Number.isInteger(json.total_pages) && page + 1 >= json.total_pages) break;\n      // Cap warning (same pattern as jibeapply/workday): the feed had more\n      // pages than we were allowed to read — surface it, with the fix.\n      if (page + 1 >= maxPages && Number.isInteger(json.total_pages) && json.total_pages > maxPages) {\n        const name = typeof fallbackCompany === 'string' && fallbackCompany ? fallbackCompany : 'a16z speedrun talent network';\n        console.error(\n          `⚠️  a16z-speedrun-talent: ${name} truncated at max_pages=${maxPages} (${out.length} of ${Number.isInteger(json.total) ? json.total : 'many'} jobs) — raise max_pages on this entry or narrow with q: for more`,\n        );\n      }","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/a16z-speedrun-talent.mjs#L152-L188","documentation":"Each page of the a16z-speedrun-talent feed must be a JSON object with a `jobs` array. After the (retried) fetch, the provider checks `json.jobs` is an array; if json is null or json.jobs is not an array, it throws, naming the page and listing the actual keys received. This is a loud-failure contract guard against a silent truncated board (a blip mid-sweep previously returned nothing for the whole provider — see #2506 — so transient errors are retried first, then this guard catches a genuine shape change).","triggerScenarios":"The a16z API returns a non-JSON body (null), an error envelope without jobs, renames `jobs` in a new API version, or returns an HTML/JSON error page. Because the fetch is retried on 429/5xx/timeout, this fires only when the body is structurally wrong, not on a transient blip.","commonSituations":"Upstream API version bump renaming the jobs field; maintenance/error JSON envelope; CDN/WAF returning a wrapped response; the board's API moved and FEED_BASE points at a stale endpoint returning a different shape.","solutions":["Read the 'got keys' list in the error to identify the actual shape (e.g. [error, message] = error envelope; [data] = renamed field).","If upstream renamed the field, update the check and normalizeSpeedrunJob in providers/a16z-speedrun-talent.mjs to the new key.","If it is an error envelope, check a16z/speedrun API status; retry later if transient.","If fetchJson returned null, investigate the network/status layer."],"exampleFix":"// If upstream renamed `jobs` → `results`:\n// before\nif (!json || !Array.isArray(json.jobs)) { throw ... }\nfor (const j of json.jobs) { ... }\n\n// after\nif (!json || !Array.isArray(json.results)) { throw ... }\nfor (const j of json.results) { ... }","handlingStrategy":"try-catch","validationCode":"// Probe a single page before a full sweep to fail early with a clear message.\nfunction isValidJobsPage(json) {\n  return !!json && Array.isArray(json.jobs);\n}\nconst probe = await ctx.fetchJson(`${FEED_BASE}?page=0&source=career-ops`);\nif (!isValidJobsPage(probe)) throw new Error('a16z feed shape unexpected; not sweeping.');","typeGuard":"/** @param {unknown} j @returns {j is { jobs: unknown[], total_pages?: number, has_more?: boolean }} */\nfunction isJobsPage(j) {\n  return !!j && typeof j === 'object' && Array.isArray((/** @type {any} */ (j)).jobs);\n}","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (/unexpected API response/.test(String(err?.message))) {\n    console.error('a16z feed shape changed — inspect received keys, update parser.');\n  }\n  throw err;\n}","preventionTips":["Pin a feed fixture in CI to catch upstream renames.","This provider already retries transient (429/5xx/timeout) failures before this guard — respect that and only act on genuine shape changes.","Log the received keys (the error does) to make diagnosis fetch-free."],"tags":["a16z","api","response-shape","provider","validation"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}