666ghj/MiroFish · error · StarHistoryError

GitHub GraphQL repeated a page cursor

Error message

GitHub GraphQL repeated a page cursor

What it means

Raised in the backfill pagination loop at scripts/star_history.py:596. After each page the script records `page.end_cursor` in `seen_page_cursors`; if a subsequent page's `endCursor` value was already seen, the traversal is going in circles and the loop aborts instead of fetching the same pages forever. This complements the per-edge cursor check and the `page_number > 10_000` safety limit.

Source

Thrown at scripts/star_history.py:596

    while True:
        page = github.fetch_stargazer_page(after)
        page_number += 1
        if initial_total is None:
            initial_total = page.total_count
            pages_required = math.ceil(initial_total / PAGE_SIZE)
            remaining_requests = max(0, pages_required - 1)
            if page.rate_remaining < remaining_requests + RATE_LIMIT_RESERVE:
                raise StarHistoryError("insufficient GitHub GraphQL rate limit for backfill")
        for edge in page.edges:
            if edge.cursor in seen_edge_cursors:
                raise StarHistoryError("GitHub GraphQL repeated an edge cursor")
            seen_edge_cursors.add(edge.cursor)
            if edge.starred_at.date() < normalized_now.date():
                daily_increments[edge.starred_at.date()] += 1

        if page.end_cursor is not None:
            if page.end_cursor in seen_page_cursors:
                raise StarHistoryError("GitHub GraphQL repeated a page cursor")
            seen_page_cursors.add(page.end_cursor)
        if not page.has_next_page:
            break
        if page.end_cursor is None:
            raise StarHistoryError("GitHub GraphQL omitted the next page cursor")
        after = page.end_cursor
        if page_number > 10_000:
            raise StarHistoryError("GitHub GraphQL exceeded the page safety limit")

    if initial_total is None:
        raise StarHistoryError("GitHub GraphQL returned no pages")
    if len(seen_edge_cursors) != initial_total:
        raise StarHistoryError("stargazer list changed or was incomplete during backfill")

    running = 0
    daily: list[dict[str, Any]] = []
    for point_day in sorted(daily_increments):
        running += daily_increments[point_day]

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Re-run the backfill later — a shrinking stargazer list mid-traversal is usually transient and a clean run succeeds when churn settles.
  2. Check the run duration: backfills that take long enough for the list to churn are the usual cause; prefer running when the repo is quiet.
  3. If writing a test gateway, make `end_cursor` advance (e.g. `f"page-{n}"`) and set `has_next_page=False` on the final page.
  4. If it reproduces consistently, dump the failing page's pageInfo via `gh api graphql` and verify whether endCursor actually repeats; that would be an upstream GitHub defect worth reporting.

Example fix

class FakeGateway:
    def fetch_stargazer_page(self, after):
        # before: end_cursor="same" on every page -> 'repeated a page cursor'
        # after: unique, advancing end cursor per page
        idx = 0 if after is None else int(after.split("-")[1]) + 1
        return StargazerPage(
            total_count=300,
            edges=make_edges(idx * 100, 100),
            has_next_page=idx < 2,
            end_cursor=f"page-{idx}" if idx < 2 else None,
            rate_remaining=5000,
        )
Defensive patterns

Strategy: retry

Try / catch

attempts = 0
while True:
    try:
        state = run_backfill(gateway, clock)
        break
    except StarHistoryError as exc:
        attempts += 1
        if attempts >= 3 or "repeated a page cursor" not in str(exc):
            raise
        logger.warning("pagination loop detected (attempt %d), restarting", attempts)

Prevention

When it happens

Trigger: The loop does `after = page.end_cursor` each iteration; if GitHub's `pageInfo.endCursor` for a `hasNextPage: true` page equals an `endCursor` already returned earlier, `page.end_cursor in seen_page_cursors` is true and the error fires. Happens when the stargazer list shrinks (stars removed) mid-traversal so GitHub re-issues a cursor, or when a gateway/test double returns a constant endCursor while claiming `has_next_page`.

Common situations: Long backfills over thousands of pages on repos with concurrent un-starring; GitHub GraphQL cursor instability under heavy mutation; broken fake gateways in the test suite that always return the same endCursor with hasNextPage=true (which would otherwise infinite-loop).

Related errors


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/2dd2fa3e2fd9f814. Report an issue: GitHub.