Skip to content

fix: harden pagination and exit-code classification - #3

Merged
bmdavis419 merged 2 commits into
mainfrom
fix/pagination-and-exit-codes
Jul 26, 2026
Merged

fix: harden pagination and exit-code classification#3
bmdavis419 merged 2 commits into
mainfrom
fix/pagination-and-exit-codes

Conversation

@bmdavis419

@bmdavis419 bmdavis419 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Backports the three behavioral bug fixes that were found and validated by differential testing during the TypeScript port audit (#2). Each was confirmed present in the Go source before fixing.

Fixes

1. No misleading resume token on --limit truncation (internal/youtube/list.go)

List() truncated the final page to honor --limit but still reported the server's nextPageToken — which points past the discarded items, so resuming from it silently skipped data. A truncated page now reports ""; a limit landing exactly on a page boundary still keeps the token. The old behavior was locked in by TestListPaginationLimitAndToken, which is updated, and the boundary case gets a new test.

2. --all pagination is bounded (internal/youtube/list.go)

The loop terminated only on an empty nextPageToken, trusting the server completely — a server repeating a token (bug, buggy proxy, or hostile endpoint) looped forever, accumulating every page in memory. During the port audit a test harness returning a constant token consumed ~59 GB of RSS before being killed. Two guards now apply:

  • a token already followed stops the loop (it can only re-fetch the same page) and reports no resume point
  • a 10,000-request ceiling returns an error suggesting --limit

Neither is reachable on a well-behaved server: at the largest page size any endpoint accepts (2000, live chat) the ceiling allows 20M items, and every other endpoint caps at 50 or 100 per page.

3. Consistent error-reason normalization in exitCode() (cmd/oytc/main.go)

The auth test stripped _/- from reasons before matching, but the quota/rate-limit test matched against the raw string — so userRateLimitExceeded correctly exited 5 while RATE_LIMIT_EXCEEDED fell through to 6. Google returns SCREAMING_SNAKE reasons on newer API surfaces, so the miss was real. One normalization now applies to every test in the table. New table cases cover QUOTA_EXCEEDED, RATE_LIMIT_EXCEEDED, rate-limit-exceeded, and userRateLimitExceeded.

Docs

docs/commands.md pagination section now documents the truncation/resume-token rule.

Release

Versioning here is tag-driven (no version file; internal/version.Version is injected via ldflags from the tag). After merge, cut the release with:

git tag v0.3.3
git push origin v0.3.3

Verification

🤖 Generated with Claude Code


Open in Devin Review

Note

Harden pagination loop termination and exit-code classification for API error reasons

  • Client.List now clears NextPageToken when a page is truncated by --limit, preserves it on exact page boundaries, and stops if the server echoes a previously seen token.
  • Adds a hard cap of MaxListRequests = 10000 paginated requests; exceeding it returns an error suggesting --limit.
  • exitCode normalizes API error reason strings by lowercasing and stripping _ and - before classifying quota/rate-limit (exit 5) and key/permission (exit 3) errors, handling variants like RATE_LIMIT_EXCEEDED or rate-limit-exceeded.
  • Behavioral Change: truncated pages no longer expose the server's nextPageToken, preventing resumption past discarded items.

Macroscope summarized 92114c2.

Greptile Summary

This PR hardens pagination and normalizes API error-reason classification.

  • Clears invalid resume tokens when a limit truncates a fetched page.
  • Stops repeated-token pagination and adds a 10,000-request ceiling.
  • Normalizes underscore and hyphen variants before assigning exit codes.
  • Adds tests and documents resume-token behavior.

Confidence Score: 3/5

The request limit should be fixed before merging because it can reject a valid --all --page-size 1 traversal and discard all fetched output.

List commands permit one item per request, so a valid result set exceeding 10,000 items reaches the new limit despite continuously advancing tokens; callers then return the error without rendering the accumulated result.

Files Needing Attention: internal/youtube/list.go

T-Rex T-Rex Logs

What T-Rex did

  • An attempt was made to verify the request ceiling behavior for valid pagination, but verification was blocked before any runtime command completed, so no runtime proof could be established; source inspection located the 10,000-request path, though it cannot serve as proof by itself.
  • I executed the contract validation test suites and observed that internal/youtube tests passed under -race and cmd/oytc tests passed under -race; the runs recorded exact commands, working directories, exit codes, test names, and PASS results in the logs.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
internal/youtube/list.go Adds truncation and loop guards, but the fixed request ceiling can terminate a valid small-page traversal and discard its output.
internal/youtube/client_test.go Adds coverage for truncation, repeated tokens, starting-token echoes, and the request ceiling, but omits a valid traversal exceeding the ceiling.
cmd/oytc/main.go Consistently normalizes API reason separators before auth, quota, and rate-limit classification.
cmd/oytc/main_test.go Adds appropriate coverage for camel-case, underscore, and hyphenated quota and rate-limit reasons.
docs/commands.md Documents when limit truncation suppresses a resume token and when an exact page boundary preserves it.

Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
internal/youtube/list.go:82-84
**Request ceiling rejects valid pagination**

When `--all` uses `--page-size 1` for more than 10,000 results, this ceiling rejects a traversal whose tokens are still advancing, causing the command to discard all accumulated output and return an error.

Reviews (2): Last reviewed commit: "fix: seed pagination loop detection with..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Three fixes, each validated by differential testing during the TypeScript
port audit (PR #2) and backported here:

- List() no longer reports a nextPageToken when --limit discarded items
  from the final fetched page. The server's token points past the
  discarded tail, so resuming from it silently skipped data. A limit
  landing exactly on a page boundary still keeps the token.

- The --all pagination loop is now bounded. Previously it terminated
  only on an empty nextPageToken, trusting the server completely; a
  server repeating a token looped forever, accumulating pages in memory
  without limit. Two guards: a repeated token stops the loop (it can
  only re-fetch the same page), and a 10,000-request ceiling returns an
  error. Neither is reachable on a well-behaved server.

- exitCode() now applies the same separator-stripping normalization to
  the quota/rate-limit reason test that the auth test already used, so
  RATE_LIMIT_EXCEEDED and QUOTA_EXCEEDED (the SCREAMING_SNAKE style
  newer Google API surfaces return) exit 5 like their camelCase
  equivalents, instead of falling through to 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Pagination behavior

Layer / File(s) Summary
Pagination control and limit semantics
internal/youtube/list.go, internal/youtube/client_test.go, docs/commands.md
Client.List clears tokens after middle-of-page truncation, preserves them at exact page boundaries, stops on repeated tokens, and enforces MaxListRequests; tests and documentation cover these behaviors.

Exit code classification

Layer / File(s) Summary
Normalized quota and rate-limit classification
cmd/oytc/main.go, cmd/oytc/main_test.go
Quota and rate-limit detection uses normalized reason strings, with coverage for multiple separator and casing formats.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title concisely summarizes the two main fixes: pagination hardening and exit-code classification.
Description check ✅ Passed The description matches the changeset and describes the pagination, exit-code, docs, and test updates.

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

coderabbitai[bot]

This comment was marked as resolved.

A server echoing the caller's --page-token back as nextPageToken is the
same loop as any other repeated token, but seenTokens started empty, so
the page was fetched and appended twice before detection fired. Seed the
set with options.PageToken and cover it with a regression test asserting
one request, no duplicated items, and no resume token.

Found by CodeRabbit on PR #3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread internal/youtube/list.go
Comment on lines +82 to +84
// again. Stop and report "", which correctly says "no valid resume
// point" rather than handing back a token that loops.
if _, seen := seenTokens[result.NextPageToken]; seen {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Request ceiling rejects valid pagination

When --all uses --page-size 1 for more than 10,000 results, this ceiling rejects a traversal whose tokens are still advancing, causing the command to discard all accumulated output and return an error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/youtube/list.go
Line: 82-84

Comment:
**Request ceiling rejects valid pagination**

When `--all` uses `--page-size 1` for more than 10,000 results, this ceiling rejects a traversal whose tokens are still advancing, causing the command to discard all accumulated output and return an error.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally skipping this one. Reaching the ceiling requires 10,000 requests, and every paginated request costs ≥1 unit of the default 10,000-unit daily quota — so the API's own quotaExceeded cuts the traversal off at or before the point the ceiling fires (and a mid-pagination quota error discards accumulated output the same way; that's the pre-existing behavior for any mid-traversal failure). Endpoints with page sizes above 100 (live chat, up to 2000) reject --all by design, so the 1-unit endpoints are the only ones that paginate. A bound has to exist somewhere to stop a non-terminating server — a legitimate 10,001st token is indistinguishable from a hostile one — and --limit is the documented escape hatch (the error message points to it). The value also stays in parity with the TypeScript port's pinned MAX_PAGES = 10000 (#2, D3).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a sound argument — the API quota acts as a natural ceiling below the guard for 1-unit endpoints, high-page-size endpoints reject --all by design, and the TypeScript port parity is a reasonable anchor. Understood, withdrawing the concern.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@bmdavis419

Copy link
Copy Markdown
Contributor Author

Bot-review triage summary — all bots green on HEAD (92114c2):

Fixed (1):

  • CodeRabbit: seenTokens didn't include the caller's initial --page-token, so a server echoing it back fetched the page twice before loop detection fired. Fixed in 92114c2 with a regression test.

Skipped (1, with rationale on the thread):

  • Greptile P1 ("request ceiling rejects valid pagination"): reaching 10,000 requests exhausts the 10,000-unit daily quota first on every endpoint that allows --all, so the ceiling isn't reachable by a legitimate traversal; --limit remains the documented escape hatch and the value matches the TS port's pinned MAX_PAGES (Port from Go to TypeScript on Effect v4 + Bun #2, D3).

CI ×3, Macroscope, CodeRabbit, Devin, Greptile: all pass.

@bmdavis419
bmdavis419 merged commit c832ad1 into main Jul 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant