jackwener/OpenCLI · error · CommandExecutionError

Zhihu search pagination returned a repeated next URL

Error message

Zhihu search pagination returned a repeated next URL

What it means

The search pager tracks every followed next URL in a `visited` set. If Zhihu's API returns a paging.next URL that was already followed, the CLI throws to prevent an infinite pagination loop (repeatedly fetching the same page). This detects buggy or wrapped pagination responses.

Solutions

  1. Re-run the search to rule out transient cache/proxy staleness
  2. Check whether a proxy/cache is stripping cursor or offset query parameters
  3. Update normalizeSearchUrl so it preserves pagination parameters
  4. Lower resultLimit to end pagination before the loop point
Defensive patterns

Strategy: validation

Validate before calling

const seen = new Set();
let probe = data.paging?.next && new URL(data.paging.next);
if (probe && seen.has(probe.href)) throw new Error('pagination would loop');
seen.add(probe.href);

Type guard

function isFreshNext(next, visited) {
  return typeof next === 'string' && !visited.has(next);
}

Try / catch

try {
  const results = await zhihuSearch(query, opts);
} catch (err) {
  if (err.message.includes('repeated next URL')) {
    console.error('Zhihu pagination is looping (cache/proxy stripping cursor params); dedupe results manually');
  } else throw err;
}

Prevention

When it happens

Trigger: paging.next points back to a previously fetched page (same normalized URL), e.g. the API ignores the offset/cursor parameter, or a proxy strips query parameters so every next link normalizes to the same URL.

Common situations: Caching layer or corporate proxy serving stale pages, Zhihu cursor parameter ignored under rate limiting, normalizeSearchUrl stripping a distinguishing query param after a change in the URL format.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/6856271687a846fe. Report an issue: GitHub.

Appendix: source

Thrown at clis/zhihu/search.js:179

            for (const item of data.data) {
                const rawType = item?.object?.type;
                if (type !== 'all' && rawType && rawType !== type) continue;
                const normalized = normalizeResultItem(item);
                if (!normalized) continue;
                if (type !== 'all' && normalized.row.type !== type) continue;
                if (seen.has(normalized.key)) continue;
                seen.add(normalized.key);
                results.push(normalized.row);
                if (results.length >= resultLimit) break;
            }
            if (results.length >= resultLimit) break;
            if (data.paging?.is_end) break;
            const next = normalizeSearchUrl(data.paging?.next);
            if (!next) {
                throw new CommandExecutionError('Zhihu search pagination returned malformed next URL');
            }
            if (visited.has(next)) {
                throw new CommandExecutionError('Zhihu search pagination returned a repeated next URL');
            }
            url = next;
        }
        if (results.length === 0) {
            throw new EmptyResultError('zhihu search', `No ${type === 'all' ? '' : `${type} `}results found for "${query}"`);
        }
        return results.map((row, i) => {
            return {
                rank: i + 1,
                ...row,
            };
        });
    },
});

export const __test__ = {
    stripHtml,
    itemKey,

View on GitHub (pinned to 49907e53dc)