{"record":{"id":"83713c5348e13179","repo":"jackwener/OpenCLI","slug":"zhihu-answer-comments-contained-a-reply-cycle-at","errorCode":null,"errorMessage":"Zhihu answer comments contained a reply cycle at ${cursor}","messagePattern":"Zhihu answer comments contained a reply cycle at (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/zhihu/answer-comments-helpers.js","lineNumber":204,"sourceCode":"        if (root.child_comment_count === 0) continue;\n        const children = await fetchChildComments(page, id, repliesLimit);\n        if (children.length === 0) {\n            throw new CommandExecutionError(`Zhihu answer root comment ${id} advertised replies but returned none`);\n        }\n        repliesByRoot.set(id, children);\n    }\n    return repliesByRoot;\n}\nfunction resolveDepths(rootId, childrenById) {\n    const depths = new Map();\n    for (const childId of childrenById.keys()) {\n        if (depths.has(childId)) continue;\n        const chain = [];\n        const active = new Set();\n        let cursor = childId;\n        let depth = 0;\n        while (cursor !== rootId && !depths.has(cursor)) {\n            if (active.has(cursor)) throw new CommandExecutionError(`Zhihu answer comments contained a reply cycle at ${cursor}`);\n            active.add(cursor);\n            chain.push(cursor);\n            const node = childrenById.get(cursor);\n            if (!node) throw new CommandExecutionError(`Zhihu answer comments referenced missing parent ${cursor}`);\n            cursor = node.descriptor.parentId;\n        }\n        if (cursor !== rootId) depth = depths.get(cursor);\n        for (let index = chain.length - 1; index >= 0; index -= 1) depths.set(chain[index], ++depth);\n    }\n    return depths;\n}\nfunction toRow(comment, descriptor, ranks, context) {\n    return {\n        rank: ranks.rank,\n        comment_rank: ranks.commentRank,\n        reply_rank: ranks.replyRank,\n        depth: ranks.depth,\n        id: descriptor.id,","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/zhihu/answer-comments-helpers.js#L186-L222","documentation":"resolveDepths computes each child's depth by walking reply_comment_id links up to the root. If the walk revisits a node already on the current path (active set), the parent links form a cycle, which would loop forever. The library throws naming the cursor id where the cycle was detected.","triggerScenarios":"Two or more child comments point at each other (or at themselves) via reply_comment_id, e.g. A.parent=B and B.parent=A, while neither is the rootId nor already resolved in depths.","commonSituations":"Corrupted or adversarially crafted API payloads, cache poisoning serving mutated rows, or manual post-processing of rows (e.g. rewriting ids for anonymization) that accidentally reused a parent id. Extremely rare with genuine Zhihu data.","solutions":["Dump the childrenById map for the failing thread and trace reply_comment_id links to find the cycle members.","Check whether any middleware rewrote ids (anonymizers/proxies) and fix the rewriting so ids stay unique and acyclic.","Refetch the thread; genuine corruption is often transient cache content.","Patch resolveDepths to cap walk length (e.g. at child count) if you must tolerate dirty data."],"exampleFix":"// before: unbounded walk\nwhile (cursor !== rootId && !depths.has(cursor)) { ... }\n// after: bounded walk as a defense in depth\nlet hops = 0;\nwhile (cursor !== rootId && !depths.has(cursor)) {\n  if (hops++ > childrenById.size) throw new CommandExecutionError(`reply chain too long at ${cursor}`);\n  ...\n}","handlingStrategy":"validation","validationCode":"// detect cycles in the parent-link graph before calling the library\nfunction hasParentCycle(children) {\n  for (const c of children) {\n    const seen = new Set([String(c.id)]);\n    let cur = c.reply_comment_id ? String(c.reply_comment_id) : null;\n    while (cur && cur !== String(c.reply_root_comment_id)) {\n      if (seen.has(cur)) return String(c.id);\n      seen.add(cur);\n      const node = children.find(x => String(x.id) === cur);\n      cur = node?.reply_comment_id ? String(node.reply_comment_id) : null;\n    }\n  }\n  return null;\n}","typeGuard":null,"tryCatchPattern":"try {\n  const tree = await buildCommentTree(answerId);\n} catch (err) {\n  if (String(err.message).includes('reply cycle')) {\n    console.error('Parent links are cyclic — payload corrupted; aborting this thread and refetching');\n    return buildCommentTree(answerId);\n  }\n  throw err;\n}","preventionTips":["Never rewrite comment ids in post-processing (anonymizers must keep links consistent).","Treat proxy/cache content as untrusted; refetch when graph invariants fail.","Add an independent cycle check on your stored snapshots before replaying them."],"tags":["data-integrity","graph-cycle","zhihu","corruption"],"backgroundTag":"cyclic-parent-reference","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}