{"record":{"id":"f37bfb8448fded49","repo":"santifer/career-ops","slug":"themuse-unexpected-api-response-on-page-page","errorCode":null,"errorMessage":"themuse: unexpected API response on page ${page} — expected { results: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]","messagePattern":"themuse: unexpected API response on page (.+?) — expected (.+?), got keys: \\[(.+?)\\]","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/themuse.mjs","lineNumber":74,"sourceCode":"      : '';\n  return { title, url, company, location };\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'themuse',\n\n  async fetch(_entry, ctx) {\n    assertMuseUrl(FEED_BASE);\n    const allResults = [];\n    // Fetch page 0 first to discover page_count, then iterate remaining pages.\n    let pageCount = 1;\n    for (let page = 0; page < pageCount; page++) {\n      const url = `${FEED_BASE}?page=${page}`;\n      // redirect:'error' prevents SSRF via server-side redirects\n      const json = await ctx.fetchJson(url, { redirect: 'error' });\n      if (!json || !Array.isArray(json.results)) {\n        throw new Error(\n          `themuse: unexpected API response on page ${page} — expected { results: [...] }, got keys: [${json ? Object.keys(json).join(', ') : 'null'}]`,\n        );\n      }\n      if (page === 0 && Number.isInteger(json.page_count) && json.page_count > 1) {\n        pageCount = Math.min(json.page_count, 100);\n      }\n      allResults.push(...json.results);\n    }\n    return allResults.map(normalizeMuseJob).filter(Boolean);\n  },\n};\n","sourceCodeStart":56,"sourceCodeEnd":86,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/themuse.mjs#L56-L86","documentation":"On each page fetch asserts the JSON has an Array 'results'. If json is null or json.results is not an array it throws and reports the actual top-level keys. The Muse API contract is { results: [...], page_count, ... }; a different shape on any page aborts the whole fetch (there is no partial-success fallback here, unlike thehub).","triggerScenarios":"The Muse API returned an error body, an HTML/JSON hybrid, a rate-limit page, or a redesigned envelope. Because page_count drives iteration, a malformed page 0 also prevents further paging.","commonSituations":"API outage, version bump that renamed 'results', rate-limiting/CAPTCHA, or the public endpoint being deprecated/moved.","solutions":["Open https://www.themuse.com/api/public/jobs?page=0 in a browser and compare keys to the error","Retry later if the API is down or rate-limiting","If the contract changed, update the results-array check and normalizeMuseJob mapping in providers/themuse.mjs"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"/** @param {unknown} json @returns {json is { results: any[] }} */\nfunction isMuseResponse(json) {\n  return !!json && typeof json === 'object' && Array.isArray(json.results);\n}","tryCatchPattern":"try {\n  const jobs = await provider.fetch(entry, ctx);\n} catch (err) {\n  if (/themuse: unexpected API response/.test(err.message)) {\n    console.warn('The Muse API shape changed or is down — skipping');\n    return [];\n  }\n  throw err;\n}","preventionTips":["Treat Muse API-shape errors as transient; log and skip in batch scans.","Monitor for the public endpoint being deprecated or rate-limited."],"tags":["api-contract","runtime","themuse","response-shape"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}