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
- Re-run the backfill later — a shrinking stargazer list mid-traversal is usually transient and a clean run succeeds when churn settles.
- 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.
- If writing a test gateway, make `end_cursor` advance (e.g. `f"page-{n}"`) and set `has_next_page=False` on the final page.
- 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
- Keep backfill runs short: the longer the pagination, the more likely concurrent un-stars re-issue a cursor.
- Never implement a custom gateway that returns a constant endCursor with has_next_page=True.
- Watch rate_remaining between retries — each restart re-consumes pages_required requests; respect RATE_LIMIT_RESERVE=20.
- Preserve both the page-cursor and edge-cursor seen-sets if you fork the loop; they are the loop-breakers.
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
- GitHub GraphQL repeated an edge cursor
- GitHub GraphQL exceeded the page safety limit
- GitHub GraphQL returned an invalid edge cursor
- GitHub GraphQL returned invalid page information
- GitHub GraphQL returned an invalid page cursor
AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14).
Data as JSON: /api/errors/2dd2fa3e2fd9f814.
Report an issue: GitHub.