{"record":{"id":"2444129f107deb26","repo":"jackwener/OpenCLI","slug":"zhihu-label-pagination-returned-a-repeated-next","errorCode":null,"errorMessage":"Zhihu ${label} pagination returned a repeated next URL","messagePattern":"Zhihu (.+?) pagination returned a repeated next URL","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/zhihu/answer-comments-helpers.js","lineNumber":139,"sourceCode":"        if (previous.descriptor.signature !== descriptor.signature) {\n            throw new CommandExecutionError(`Zhihu answer comments returned conflicting data for comment ${descriptor.id}`);\n        }\n        const oldCount = previous.comment.child_comment_count;\n        const newCount = comment.child_comment_count;\n        if (Number.isInteger(newCount) && (!Number.isInteger(oldCount) || newCount > oldCount)) {\n            previous.comment = { ...previous.comment, child_comment_count: newCount };\n        }\n    }\n}\nasync function fetchPages(page, options) {\n    const { firstUrl, limit, label, role, expectedRootId = '', normalizeNext, notFoundDetail = '' } = options;\n    const byId = new Map();\n    const visited = new Set();\n    const maxPages = Math.ceil(limit / PAGE_SIZE) + PAGE_OVERLAP_ALLOWANCE;\n    let pageCount = 0;\n    let url = firstUrl;\n    while (byId.size < limit) {\n        if (visited.has(url)) throw new CommandExecutionError(`Zhihu ${label} pagination returned a repeated next URL`);\n        if (pageCount >= maxPages) throw new CommandExecutionError(`Zhihu ${label} pagination exceeded its fetch budget`);\n        visited.add(url);\n        pageCount += 1;\n        const payload = await fetchCommentPage(page, url, label, notFoundDetail);\n        addPageComments(byId, payload.data, role, expectedRootId);\n        if (payload.paging.is_end || byId.size >= limit) break;\n        url = normalizeNext(payload.paging.next);\n        if (!url) throw new CommandExecutionError(`Zhihu ${label} pagination returned a malformed next URL`);\n    }\n    return [...byId.values()].slice(0, limit).map(({ comment }) => comment);\n}\nexport function fetchRootComments(page, answerId, order, limit) {\n    const apiOrder = order === 'latest' ? 'ts' : 'score';\n    const path = `/api/v4/comment_v5/answers/${answerId}/root_comment`;\n    return fetchPages(page, {\n        firstUrl: `https://www.zhihu.com${path}?order_by=${apiOrder}&limit=${PAGE_SIZE}&offset=`,\n        limit,\n        label: 'answer root comments',","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/zhihu/answer-comments-helpers.js#L121-L157","documentation":"fetchPages tracks every paging URL it has requested. If paging.next leads back to an already-visited URL, the API's pagination is looping and would never terminate, so the library throws. This is a guard against Zhihu's paging.next returning a stale or self-referential link.","triggerScenarios":"payload.paging.next normalizes (normalizeNext) to a URL already in the visited set — typically because is_end never became true and next kept pointing at the same or an earlier page.","commonSituations":"Zhihu API bug for a given sort order (e.g. order param not honored so the cursor never advances), proxy stripping cursor query params so next resolves to page 1, or an incorrect answerId/order combination making the server return a static next link.","solutions":["Print each requested URL to identify which cursor link loops back and what param is missing.","Verify the order argument ('latest' vs score) is supported; try the other order.","Bypass any proxy/rewriter that strips or reorders query parameters (the cursor token is part of the URL).","Retry later if it's a transient Zhihu pagination bug; check whether the answer's comment count changed mid-fetch."],"exampleFix":"// before: rewriting next through a proxy that drops the cursor\nconst next = resp.paging.next.replace('https://api.zhihu.com', MY_PROXY); // cursor lost\n// after: pass the cursor query through untouched\nconst next = normalizeNext(resp.paging.next);","handlingStrategy":"retry","validationCode":"// sanity-check a pagination stream yourself before trusting it\nfunction detectLoop(urls) {\n  const seen = new Set();\n  for (const u of urls) {\n    if (seen.has(u)) return u;\n    seen.add(u);\n  }\n  return null;\n}","typeGuard":null,"tryCatchPattern":"try {\n  const comments = await fetchRootComments(page, answerId, order, limit);\n} catch (err) {\n  if (String(err.message).includes('repeated next URL')) {\n    console.warn('Zhihu pagination looped; retrying with the other sort order');\n    return fetchRootComments(page, answerId, order === 'latest' ? 'score' : 'latest', limit);\n  }\n  throw err;\n}","preventionTips":["Never rewrite Zhihu paging URLs through proxies that drop query params.","Keep the order argument a supported value; verify cursor behavior when changing it.","Cap your own page-walk with a visited-URL set as an independent watchdog."],"tags":["pagination","api","network","zhihu"],"backgroundTag":"pagination-loop","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}