{"record":{"id":"fb6759b63221af13","repo":"jackwener/OpenCLI","slug":"semanticscholar-recommendations-returned-an-unexpe","errorCode":null,"errorMessage":"semanticscholar recommendations returned an unexpected payload shape","messagePattern":"semanticscholar recommendations returned an unexpected payload shape","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/semanticscholar/recommendations.js","lineNumber":39,"sourceCode":"    access: 'read',\n    description: 'Semantic Scholar AI-curated related papers for a paperId, DOI, or arXiv id',\n    domain: 'api.semanticscholar.org',\n    strategy: Strategy.PUBLIC,\n    browser: false,\n    args: [\n        { name: 'id', positional: true, required: true, help: 'paperId (40-char hex), DOI, arXiv id, or prefixed id' },\n        { name: 'limit', type: 'int', default: 10, help: 'Max recommendations (1-500)' },\n    ],\n    columns: ['rank', 'paperId', 'doi', 'title', 'year', 'firstAuthor', 'citationCount', 'url'],\n    func: async (args) => {\n        const ref = requirePaperRef(args.id);\n        const limit = requireBoundedInt(args.limit, 10, 500);\n        const url = `${S2_REC_BASE}/papers/forpaper/${encodeURIComponent(ref)}?fields=${FIELDS}&limit=${limit}`;\n        const body = await s2Fetch(url, 'semanticscholar recommendations');\n\n        const recommended = Array.isArray(body?.recommendedPapers) ? body.recommendedPapers : null;\n        if (recommended === null) {\n            throw new CommandExecutionError('semanticscholar recommendations returned an unexpected payload shape');\n        }\n        if (!recommended.length) {\n            throw new EmptyResultError('semanticscholar recommendations', `No Semantic Scholar recommendations for \"${args.id}\".`);\n        }\n\n        return recommended.slice(0, limit).map((p, i) => normalizePaperRow(p, 'recommendations', { rank: i + 1 }));\n    },\n});\n","sourceCodeStart":21,"sourceCodeEnd":48,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/semanticscholar/recommendations.js#L21-L48","documentation":"The recommendations command (clis/semanticscholar/recommendations.js:39) calls the `/recommendations/v1/papers/forpaper` endpoint and expects a `recommendedPapers` array in the response. If that key is missing or not an array, this CommandExecutionError is thrown because the recommendation payload contract was violated.","triggerScenarios":"The recommendations endpoint returned 200 with JSON lacking recommendedPapers — e.g. a throttling/soft-error envelope, an error object for an unknown paperId, or an API schema change in the recommendations service.","commonSituations":"Semantic Scholar rate-limiting the recommendations endpoint (which is stricter than the graph API); recommending from a paper id the service cannot resolve; a proxy or cache substituting its own JSON body; recommendations API version changes.","solutions":["Print the raw response body to see what replaced recommendedPapers.","Retry with backoff — the recommendations endpoint throttles aggressively and often returns soft errors with 200.","Confirm the input paper is valid via `semanticscholar paper <id>` first.","Check the Semantic Scholar recommendations API docs for endpoint/shape changes (it is versioned separately from the graph API).","Rule out proxies/caches rewriting the response."],"exampleFix":"// before\nconst body = await s2Fetch(url, 'semanticscholar recommendations');\nconst recommended = body?.recommendedPapers ?? [];\n// after\nconst body = await s2Fetch(url, 'semanticscholar recommendations');\nif (body && typeof body === 'object' && body.error) {\n  throw new Error(`S2 recommendations soft error: ${body.error} — backoff and retry`);\n}\nconst recommended = Array.isArray(body?.recommendedPapers) ? body.recommendedPapers : [];","handlingStrategy":"type-guard","validationCode":"function hasRecommendationsEnvelope(body) {\n  return body !== null && typeof body === 'object' && Array.isArray(body.recommendedPapers);\n}","typeGuard":"function isRecommendationsPayload(body) {\n  return typeof body === 'object' && body !== null && Array.isArray(body.recommendedPapers)\n    && body.recommendedPapers.every((p) => p && typeof p === 'object' && 'paperId' in p);\n}","tryCatchPattern":"try {\n  recs = await recommendations(id);\n} catch (err) {\n  if (/unexpected payload shape/.test(err.message)) {\n    // recommendations endpoint throttles hard: exponential backoff\n    await sleep(5000);\n    recs = await recommendations(id);\n  } else throw err;\n}","preventionTips":["Rate-limit recommendation calls well below graph-API limits; this endpoint throttles first.","Validate input paper ids via the paper endpoint before recommending.","Inspect raw bodies on shape failures — soft throttling errors often lack recommendedPapers.","Track the recommendations API version separately from the graph API."],"tags":["api","schema","semanticscholar","response-shape","rate-limit"],"backgroundTag":"unexpected-api-payload-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}