666ghj/MiroFish · error · StarHistoryError

GitHub GraphQL omitted the next page cursor

Error message

GitHub GraphQL omitted the next page cursor

What it means

pageInfo.hasNextPage was true but endCursor was null or an empty string. Without a cursor there is no way to request the next page, so continuing would silently loop the first page forever; the parser raises StarHistoryError('GitHub GraphQL omitted the next page cursor') to fail fast instead.

Source

Thrown at scripts/star_history.py:290

        for raw_edge in raw_edges:
            if not isinstance(raw_edge, dict):
                raise StarHistoryError("GitHub GraphQL returned an invalid edge")
            cursor = raw_edge.get("cursor")
            starred_at = raw_edge.get("starredAt")
            if not isinstance(cursor, str) or not cursor:
                raise StarHistoryError("GitHub GraphQL returned an invalid edge cursor")
            if not isinstance(starred_at, str):
                raise StarHistoryError("GitHub GraphQL returned an invalid star timestamp")
            edges.append(StargazerEdge(cursor, _parse_github_timestamp(starred_at)))

        has_next_page = page_info.get("hasNextPage")
        end_cursor = page_info.get("endCursor")
        if type(has_next_page) is not bool:
            raise StarHistoryError("GitHub GraphQL returned invalid page information")
        if end_cursor is not None and not isinstance(end_cursor, str):
            raise StarHistoryError("GitHub GraphQL returned an invalid page cursor")
        if has_next_page and not end_cursor:
            raise StarHistoryError("GitHub GraphQL omitted the next page cursor")

        return StargazerPage(
            total_count=total_count,
            edges=tuple(edges),
            has_next_page=has_next_page,
            end_cursor=end_cursor,
            rate_remaining=rate_remaining,
        )


def _strict_non_negative_int(value: Any, label: str) -> int:
    if type(value) is not int or value < 0:
        raise StarHistoryError(f"{label} must be a non-negative integer")
    return value


def _parse_github_timestamp(value: str) -> datetime:
    try:

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Give the fixture a non-empty endCursor whenever hasNextPage is true
  2. Or set hasNextPage: false when the fixture has no cursor
  3. Verify no code path strips endCursor from the response before parsing

Example fix

// before (fixture)
"pageInfo": {"hasNextPage": true, "endCursor": null}

// after (fixture)
"pageInfo": {"hasNextPage": true, "endCursor": "Y3Vyc29yOnYyODI5NQ=="}
Defensive patterns

Strategy: validation

Validate before calling

if page_info.get("hasNextPage") is True and not page_info.get("endCursor"):
    raise StarHistoryError("hasNextPage without endCursor — pagination would repeat the page")

Type guard

def next_page_navigable(page_info: dict) -> bool:
    return page_info.get("hasNextPage") is False or bool(page_info.get("endCursor"))

Try / catch

try:
    page = _parse_graphql_page(payload)
except StarHistoryError as exc:
    if "omitted the next page cursor" in str(exc):
        raise StarHistoryError("stopping pagination: server signalled more pages but gave no cursor") from exc
    raise

Prevention

When it happens

Trigger: Response with hasNextPage: true and endCursor: null (or "") — usually a fixture that set hasNextPage without providing a cursor, or a stubbed server; on the real API this combination is effectively impossible.

Common situations: Test fixtures exercising the 'more pages' path without copying a real cursor; a mock that defaults endCursor to null.

Related errors


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