666ghj/MiroFish · error · StarHistoryError

GitHub returned an invalid pagination cursor

Error message

GitHub returned an invalid pagination cursor

What it means

Defensive validation of the GraphQL pagination cursor before it is injected into the gh -f after= argument: the cursor must be non-empty and contain no \n or \r. The newline ban keeps the subprocess argument clean (no argument smuggling) and rejects corrupted cursor strings. The cursor normally comes from a previous response's pageInfo.endCursor, so failure means GitHub returned a garbage cursor or persisted state was corrupted.

Source

Thrown at scripts/star_history.py:231

    def __init__(self, runner: CommandRunner | None = None) -> None:
        self._runner = runner or SubprocessCommandRunner()

    def fetch_stargazer_page(self, after: str | None) -> StargazerPage:
        arguments = [
            "gh",
            "api",
            "graphql",
            "-f",
            f"query={GRAPHQL_QUERY}",
            "-f",
            f"owner={REPOSITORY_OWNER}",
            "-f",
            f"name={REPOSITORY_NAME}",
        ]
        if after is not None:
            if not after or "\n" in after or "\r" in after:
                raise StarHistoryError("GitHub returned an invalid pagination cursor")
            arguments.extend(("-f", f"after={after}"))

        completed = self._runner.run(arguments)
        if completed.returncode != 0:
            raise StarHistoryError(
                f"GitHub GraphQL request failed (exit {completed.returncode})"
            )
        try:
            payload = json.loads(completed.stdout)
        except json.JSONDecodeError as exc:
            raise StarHistoryError("GitHub GraphQL returned malformed JSON") from exc

        if not isinstance(payload, dict) or payload.get("errors"):
            raise StarHistoryError("GitHub GraphQL rejected the stargazer request")
        try:
            data = payload["data"]
            repository = data["repository"]
            stargazers = repository["stargazers"]

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Inspect the cursor value at the failure point (log it repr()'d) — empty string usually means pagination should have stopped, so verify the loop's hasNextPage handling.
  2. If resuming from corrupted state, remove the bad cursor from the persisted history and re-run the backfill page fetch from the last known-good cursor.
  3. Never hand-edit cursors in history.json; treat them as opaque tokens exactly as the docstring requires.
Defensive patterns

Strategy: validation

Validate before calling

def is_usable_cursor(cursor: object) -> bool:
    """Opaque GraphQL cursor: non-empty string, single line."""
    return (
        isinstance(cursor, str)
        and cursor != ""
        and "\n" not in cursor
        and "\r" not in cursor
    )

# before passing a cursor read from persisted state
assert is_usable_cursor(after), f"corrupted cursor in history: {after!r}"

Try / catch

try:
    page = gateway.fetch_stargazer_page(after)
except StarHistoryError as exc:
    if "invalid pagination cursor" in str(exc):
        # discard the suspect cursor and restart pagination from None
        page = gateway.fetch_stargazer_page(None)
    else:
        raise

Prevention

When it happens

Trigger: GitHub's pageInfo.endCursor comes back as an empty string (can happen when hasNextPage is erroneously true); the local history state file was hand-edited so a stored cursor is empty or contains a newline; a GraphQL response shape change puts a non-string into the cursor chain.

Common situations: Resuming a backfill from a manually patched history.json; truncated cursor copied by hand; upstream API anomaly during pagination.

Related errors


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