{"record":{"id":"0821bb18706f166d","repo":"666ghj/MiroFish","slug":"github-returned-an-invalid-pagination-cursor","errorCode":null,"errorMessage":"GitHub returned an invalid pagination cursor","messagePattern":"GitHub returned an invalid pagination cursor","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"error","filePath":"scripts/star_history.py","lineNumber":231,"sourceCode":"\n    def __init__(self, runner: CommandRunner | None = None) -> None:\n        self._runner = runner or SubprocessCommandRunner()\n\n    def fetch_stargazer_page(self, after: str | None) -> StargazerPage:\n        arguments = [\n            \"gh\",\n            \"api\",\n            \"graphql\",\n            \"-f\",\n            f\"query={GRAPHQL_QUERY}\",\n            \"-f\",\n            f\"owner={REPOSITORY_OWNER}\",\n            \"-f\",\n            f\"name={REPOSITORY_NAME}\",\n        ]\n        if after is not None:\n            if not after or \"\\n\" in after or \"\\r\" in after:\n                raise StarHistoryError(\"GitHub returned an invalid pagination cursor\")\n            arguments.extend((\"-f\", f\"after={after}\"))\n\n        completed = self._runner.run(arguments)\n        if completed.returncode != 0:\n            raise StarHistoryError(\n                f\"GitHub GraphQL request failed (exit {completed.returncode})\"\n            )\n        try:\n            payload = json.loads(completed.stdout)\n        except json.JSONDecodeError as exc:\n            raise StarHistoryError(\"GitHub GraphQL returned malformed JSON\") from exc\n\n        if not isinstance(payload, dict) or payload.get(\"errors\"):\n            raise StarHistoryError(\"GitHub GraphQL rejected the stargazer request\")\n        try:\n            data = payload[\"data\"]\n            repository = data[\"repository\"]\n            stargazers = repository[\"stargazers\"]","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L213-L249","documentation":"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.","triggerScenarios":"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.","commonSituations":"Resuming a backfill from a manually patched history.json; truncated cursor copied by hand; upstream API anomaly during pagination.","solutions":["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.","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.","Never hand-edit cursors in history.json; treat them as opaque tokens exactly as the docstring requires."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"def is_usable_cursor(cursor: object) -> bool:\n    \"\"\"Opaque GraphQL cursor: non-empty string, single line.\"\"\"\n    return (\n        isinstance(cursor, str)\n        and cursor != \"\"\n        and \"\\n\" not in cursor\n        and \"\\r\" not in cursor\n    )\n\n# before passing a cursor read from persisted state\nassert is_usable_cursor(after), f\"corrupted cursor in history: {after!r}\"","typeGuard":null,"tryCatchPattern":"try:\n    page = gateway.fetch_stargazer_page(after)\nexcept StarHistoryError as exc:\n    if \"invalid pagination cursor\" in str(exc):\n        # discard the suspect cursor and restart pagination from None\n        page = gateway.fetch_stargazer_page(None)\n    else:\n        raise","preventionTips":["Treat cursors as opaque — never hand-edit or reformat them in history.json.","Validate persisted cursors with the same single-line/non-empty rule before resuming.","Cross-check hasNextPage before using endCursor; empty endCursor with hasNextPage true is the usual corruption signal."],"tags":["graphql","pagination","cursor","validation","star-history"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}