Skip to content

feat(client-generator): follow the next page URL from the response body in link-style pagination - #3230

Open
kamil1094 wants to merge 1 commit into
mainfrom
feat/client-generator-body-link-pagination
Open

kamil1094 wants to merge 1 commit into
mainfrom
feat/client-generator-body-link-pagination

Conversation

@kamil1094

@kamil1094 kamil1094 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What/Why/How?

What: link-style pagination gets an optional nextLink JSON pointer. With it, the generated clients read the URL of the next page from a response field instead of the Link header:

client:
  pagination:
    style: link
    nextLink: /page/nextPage
    items: /data

Why: some APIs return the next page as a URL in the body (Stripe v2 next_page_url, and Reunite's Main API is moving page.nextPage from an opaque cursor to a ready-to-call path such as /orgs/o1/projects?limit=10&after=…). The four existing styles can't follow that:

  • cursor copies the whole URL into the cursor parameter.
  • link only reads the header.

How: the body URL goes through the same path as a Link header target. It resolves against the page URL, its query parameters merge into the next call to the same declared operation, and the existing repeat guard applies.

  • Resolver: shape check ("nextLink" must be a JSON pointer…) and fit check (the pointer must resolve to a string in the JSON success response). A link convention with nextLink fits the operations that resolve the pointer, so it needs no documented Link header. The language-neutral paginationRuleFor takes an optional model for the same check, and the reference page passes it.
  • Runtimes: TypeScript (pagesByLink now takes the spec, like pages), Python (sync and async), Go and PHP read the pointer when it is set. A non-string value throws in TS, Python and Go. PHP stops, as it already does for a bad cursor.
  • tanstack-query: a body-link query operation that has a query parameter now gets <op>InfiniteOptions. The URL is the page param, and later pages merge its query parameters over vars through a small module-private nextLinkQuery helper. Header links still get none, since a queryFn can't see headers. The generator skill (AGENTS.md) is updated first, as it requires.
  • Config: nextLink is added to the redocly.yaml pagination rule schema in core.
  • Go fix (separate changeset): the link merge used params.Add, so a key present in both the caller's params and the link target, such as limit, was sent twice. It now replaces per key, like the TS, Python and PHP runtimes.

Reference

Needed to switch Reunite's generated SDK (.items() and the TanStack infinite queries) to URL-valued page.nextPage.

Testing

  • Unit: resolver shape and fit (src/__tests__/pagination.test.ts), paginationRuleFor convention fit, pagesByLink following the body pointer and rejecting a non-string, and the TanStack render output.
  • E2E: added a listReceipts body-link operation to fixtures/pagination.yaml.
    • pagination.test.ts walks .items() over the live server. The wire log shows limit once per request: /receipts?limit=2, …&after=2, …&after=4.
    • tanstack-query.test.ts strict-tsc-checks useInfiniteQuery(listReceiptsInfiniteOptions(...)) against the real @tanstack/react-query.
  • Regenerated the runtime embeds, eject skills, zero-install-quickstart example, cafe.snapshot.ts and the core config-schema snapshot.
  • Locally: npm run typecheck, npm run lint (0 errors), the full unit suite, and the touched e2e files (pagination, tanstack-query, cafe, examples, python, docs, eject).
  • Not run locally: the Go and PHP compile bars. There's no go or php toolchain on my machine, so I'm relying on the client-generators CI job for them.

Check yourself

  • This PR follows the contributing guide
  • All new/updated code is covered by tests
  • Core code changed? - Tested with other Redocly products (internal contributions only)
  • New package installed? - Tested in different environments (browser/node)
  • Documentation update has been considered

Security

  • The security impact of the change has been considered
  • Code follows company security practices and guidelines

The body URL is never requested as-is. Only its query parameters reach the next call to the same declared operation, so auth, middleware and serverUrl apply unchanged and credentials can't leak to another origin. This is the same guarantee the header link already has.


Note

Medium Risk
Changes pagination behavior across multiple language runtimes and TanStack codegen; incorrect merge or pointer handling could break paging for APIs using body URLs, though existing header-link behavior is preserved when nextLink is omitted.

Overview
Adds optional nextLink to link-style pagination so generated clients can advance using a next-page URL in the JSON response (e.g. next_page_url) instead of relying on the RFC 8288 Link header. Convention rules with nextLink attach only when that pointer resolves to a string on the operation’s success schema; generate-time validation enforces pointer shape and type.

SDK runtimes (TypeScript, Python, Go, PHP) read the pointer when set, merge the link’s query params into the next call to the same endpoint (unchanged security model), and stop on absent/null/empty values. pagesByLink now takes the pagination spec in TypeScript. Go also fixes link pagination merging query keys by replacement so repeated params (e.g. limit) are not sent twice.

TanStack Query generation emits <op>InfiniteOptions for body-link paginated GETs that have query parameters (page param = next URL, nextLinkQuery merges query from the link); header-only link pagination still has no infinite factory because queryFn cannot see headers.

Docs, Redocly config schema, e2e fixture (listReceipts), and unit/e2e tests cover resolver behavior, runtime iterators, and infinite-query output.

Reviewed by Cursor Bugbot for commit 2c4c63f. Bugbot is set up for automated code reviews on this repo. Configure here.

@changeset-bot

changeset-bot Bot commented Oct 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2c4c63f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@redocly/client-generator Minor
@redocly/cli Minor
@redocly/openapi-core Minor
@redocly/recheck Minor
@redocly/respect-core Minor
@redocly/reunite-integration Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

📝 Recheck Summary

⚠️ Warnings found • Scanned 2 changed file(s)

  • 0 error(s)
  • 45 warning(s)
  • 25 info

📋 Findings by rule

🟡 technical-english/paragraph-length: 0 errors, 19 warnings, 0 info
🟡 plain-language/paragraph-sentence-count: 0 errors, 10 warnings, 0 info
🟡 technical-english/sentence-length: 0 errors, 15 warnings, 0 info
🔵 technical-english/passive-voice: 0 errors, 0 warnings, 25 info
🟡 page-reading-time: 0 errors, 1 warnings, 0 info

Errors fail the full-tree check. Warnings and info are a worklist and do not block the PR.

📖 Readability of changed files

Automated Readability Index (ARI): a U.S. grade level. Lower is easier to read.

File Base This PR Change
docs/@v2/configuration/reference/client.md 7.0 6.8 🟢 -0.2 easier
docs/@v2/guides/use-generated-client.md 7.3 7.2 ±0

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 83.17% (🎯 80%) 20446 / 24581
🔵 Statements 82.74% (🎯 80%) 22081 / 26684
🔵 Functions 84.4% (🎯 80%) 3849 / 4560
🔵 Branches 75.28% (🎯 75%) 14908 / 19801
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/client-generator/src/pagination.ts 98.46% 98.1% 100% 99.11% 328, 360
packages/client-generator/src/authoring/pagination.ts 100% 91.48% 100% 100%
packages/client-generator/src/authoring/reference-page.ts 93.47% 78.57% 100% 92.85% 54, 60, 67, 71-75
packages/client-generator/src/generators/go/descriptor.ts 100% 56.25% 100% 100%
packages/client-generator/src/generators/php/descriptor.ts 94.73% 66.66% 100% 94.44% 49
packages/client-generator/src/generators/python/descriptor.ts 93.33% 56.25% 100% 100% 43
packages/client-generator/src/generators/tanstack-query/render.ts 98.98% 90% 100% 100% 379
packages/client-generator/src/generators/typescript/runtime/create-client.ts 82.85% 78.44% 91.42% 85.25% 109-135, 148, 286, 422-427, 513-518
packages/client-generator/src/generators/typescript/runtime/paginate.ts 97.82% 95.4% 100% 100% 143, 194
packages/client-generator/src/runtime-sources/go.ts 100% 100% 100% 100%
packages/client-generator/src/runtime-sources/php.ts 100% 100% 100% 100%
packages/client-generator/src/runtime-sources/python.ts 100% 100% 100% 100%
packages/client-generator/src/runtime-sources/typescript.ts 100% 100% 100% 100%
packages/core/src/types/redocly-yaml.ts 89.21% 77.35% 92.85% 88.88% 494, 526, 532, 568-575, 577, 716-726, 736-752
Generated in workflow #12481 for commit 2c4c63f by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Performance Benchmark (Lower is Faster)

CLI Version Bundle Lint Check Config
latest ▓░░░░░░░░░░ 1.00x ▓░░░░░░░░░░ 1.01x ± 0.01 ▓░░░░░░░░░░ 1.00x
next ▓░░░░░░░░░░ 1.02x ± 0.01 ▓░░░░░░░░░░ 1.00x ▓░░░░░░░░░░ 1.01x ± 0.03

@kamil1094
kamil1094 requested a review from rudi23 October 9, 2026 13:06
@kamil1094
kamil1094 marked this pull request as ready for review October 9, 2026 13:06
@kamil1094
kamil1094 requested review from a team as code owners October 9, 2026 13:06

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2c4c63f. Configure here.

? model !== undefined &&
page !== undefined &&
schemaAtPointer(page.schema, nextLink, model) !== undefined
: op.successResponseHeaders?.some((header) => header.name === 'link') === true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Convention fit ignores string type

Medium Severity

A link convention with nextLink is treated as a fit whenever schemaAtPointer returns a schema. The TypeScript resolver only accepts a string (including nullable strings) and skips anything else. A convention can therefore paginate operations whose field is a number, object, or array. Iteration then throws after the first page, or stops in PHP.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2c4c63f. Configure here.

@rudi23

rudi23 commented Oct 9, 2026

Copy link
Copy Markdown

what about previousPage?

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.

2 participants