Skip to content

feat(page size reduction): rewrite the page size inside a RequestPath page token URL - #1169

Draft
Anatolii Yatsuk (tolik0) wants to merge 15 commits into
mainfrom
tolik0/cdk/reduce-page-size-request-path
Draft

Anatolii Yatsuk (tolik0) wants to merge 15 commits into
mainfrom
tolik0/cdk/reduce-page-size-request-path

Conversation

@tolik0

Copy link
Copy Markdown
Contributor

Stacked on #1149 — review that one first; this branch targets tolik0/cdk/reduce-page-size and its diff is the last commit only.

What

Lets page_size_reduction be used on a paginator whose page_token_option is a RequestPath, by rewriting the page size inside the URL the API returned for the next page instead of sending the reduced page size next to the one that URL already carries. Opted into with a new rewrite_page_size_in_page_token_url flag on PageSizeReduction.

Why

#1149 rejects a RequestPath page token at config time. The reason is real, and reproducible on main today:

HttpClient(name="t", logger=logger)._create_prepared_request(
    http_method="GET",
    url="https://airbyte.io/events?per_page=100&start_time=1234",
    params={"per_page": 50},
    dedupe_query_params=True,
)
# https://airbyte.io/events?per_page=100&start_time=1234&per_page=50

HttpClient._dedupe_query_params drops an injected parameter only when the URL already carries the same value — which is exactly the case where there is nothing to reduce. So with a RequestPath token the request goes out asking for both page sizes and the API picks; every page after the first keeps asking for the size that just failed.

27 connectors in the monorepo pair a RequestPath page token with a page_size_option. The near-term adopter is source-zendesk-support ticket_comments (manifest.yaml:984-995): CursorPagination on next_page, page_token_option: RequestPath, page_size_option: per_page, and a 504 → RETRY filter that retries at an unchanged per_page so the retries cannot succeed.

One correction to the #1149 description, which said 3 of those 27 (mendeley, sendgrid, zendesk-support) already handle 502/504: grepping the current manifests, only zendesk-support has any 5xx response filter — mendeley and sendgrid have none. Structurally this unlocks 27 connectors; today exactly one of them has an error path to adopt it for, and that path is a gateway timeout rather than an explicit "page too large" signal. Worth weighing before merging.

How

  • DefaultPaginator.path takes page_size_override and rewrites the parameter named by page_size_option inside the token URL. Only that pair is edited; the rest of the query is kept byte for byte rather than parsed and re-encoded, so a URL the API built is handed back as it was written (percent-encoding, blank values and bare flags survive).
  • A URL that does not carry the parameter is left alone — get_request_params then adds the reduced page size the usual way, and appending it to the path would send it twice.
  • The reduced page size reaches path through SimpleRetriever._paginator_path, passed with page_size_override_kwargs so a paginator defined outside the CDK that does not accept the argument keeps working. PaginatorTestReadDecorator forwards it the same way.
  • After the rewrite the URL and the injected parameter agree, so the existing _dedupe_query_params drops the injected one and a single page size goes out. There is a test asserting exactly that, next to one asserting the duplicated URL without the rewrite.

Why it is opt-in

Whether a page token URL can be re-requested at a smaller page size is a property of the API, and a RequestPath token is opaque to the CDK. It holds for a URL addressing records by cursor or timestamp; it does not hold for one carrying a page number, where a smaller page size moves every following page boundary and skips records — the same hazard that makes #1149 reject PageIncrement. So the connector asserts it rather than the CDK assuming it.

Rejected at config time:

  • rewrite_page_size_in_page_token_url without a RequestPath page token — there is no URL to rewrite in.
  • rewrite_page_size_in_page_token_url with a page_size_option that does not inject into request_parameter — only a query parameter of that URL can be rewritten.
  • A RequestPath page token without the flag — the existing feat: add REDUCE_PAGE_SIZE response action for dynamic page-size reduction #1149 error, now naming the flag and what has to be true about the API to set it.

Scope

Nothing changes for a stream without page_size_reduction: page_size_override is None for every other stream, and path returns the token unchanged as before. The URL rewrite is reachable only from a reduced page.

Testing

  • DefaultPaginator.path: rewrite, absent parameter, no query, no override, non-request_parameter injection, repeated parameter, a parameter whose name is a prefix of the page size field, an interpolated field_name, a fragment, other parameters returned byte for byte.
  • End-to-end through HttpClient._create_prepared_request: one page size out after the rewrite, two without it.
  • SimpleRetriever: the override reaches path, is omitted when there is none, and _fetch_next_page sends the rewritten URL.
  • Factory: the three rejections above and a manifest that parses with the flag set.
  • Full unit_tests/sources/declarative and unit_tests/sources/streams/http green (2468 passed); the two memory-usage tests were skipped locally, they SIGILL on this machine independently of this branch.

No connector adopts this yet; source-zendesk-support would need its 504 filter switched to REDUCE_PAGE_SIZE and an SDM prerelease image, since it is manifest-only.

…ction

An HttpClient retry re-sends the same PreparedRequest, so an error handler that
mutates the stream's page size can never affect the request being retried: the
oversized page size is already serialized into the URL or the body. This adds a
`REDUCE_PAGE_SIZE` response action and a `page_size_reduction` block on
`SimpleRetriever` so a manifest can say "re-issue this page smaller" instead.

On a matching response, `SimpleRetriever._read_pages` shrinks the page size and
re-issues the same page. All reduction state lives in a `PageSizeReducer` created
per `_read_pages` call, mirroring `PaginationTracker`, because one retriever and
one paginator are shared by every partition of a stream and partitions are read
concurrently.

The reduced page size reaches the paginator and the pagination strategy as an
appended `page_size_override: Optional[int] = None` keyword argument, built by
`page_size_override_kwargs` so it is omitted when there is no reduction. A
paginator or strategy defined outside the CDK keeps working unchanged.

Stop conditions see the size that was actually requested:

- `OffsetIncrement` compares `last_page_size` against the requested size.
- `CursorPagination` exposes it to `stop_condition` and `cursor_value` as
  `page_size`, and the factory rejects a `stop_condition` comparing
  `last_page_size` against anything else, because a full page at the reduced size
  would otherwise read as a short page and end the pagination silently.
- `PageIncrement` is rejected, including through a subclass: pages are addressed
  as page number * page size, so a smaller page shifts every following boundary.

Termination is bounded by `max_attempts` reductions. That budget restarts after
every successful page under `reset_policy: AFTER_SUCCESSFUL_PAGE`, which is the
policy for an API that rejects the configured page size on every page - without
the restart such a stream would fail at page `max_attempts + 1` however healthy
the reads are. `PageSizeReducer.MAX_TOTAL_REDUCTIONS` bounds the partition either
way, and each reduction waits a short, growing amount of time before re-issuing.

The action is rejected at config time everywhere it cannot be honored: query
properties, `file_uploader`, `lazy_read_pointer`, all six `AsyncRetriever`
sub-requesters and `login_requester`. A `CustomPaginationStrategy` is accepted
only when its `next_page_token` can receive the override. When the action reaches
a retriever the factory could not inspect, the reduction fails with a config error
naming the stream rather than a bare crash.

`HttpClient` logs a response resolving to `REDUCE_PAGE_SIZE` as an auxiliary
request, so a Connector Builder test read still shows the failed attempt without
counting it against `max_pages_per_slice`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… failing healthy long partitions

Three round-2 review findings, two of them introduced by the round-1 fixes.

The `stop_condition` gate matched the expression as a string, and a regex
cannot do that job in either direction. `\bpage_size\b` matches inside
`config['page_size']` because `'` is a non-word character, so
`{{ last_page_size < config['page_size'] }}` passed the gate and still
silently truncated - the exact bug the gate exists to prevent. And
`{{ last_page_size == 0 }}` was rejected although no reduction can make a
full page empty; that is 11 of the 14 `last_page_size` stop conditions in
the monorepo, including all 6 in source-zendesk-support, which this PR
names as an adopter. Patching the regex was not the fix: two bugs in one
pattern meant the mechanism was wrong.

The condition is now parsed as a Jinja expression and the comparisons
`last_page_size` takes part in are classified on the AST, where a bare
`page_size` Name is distinguishable from a Getitem on `config`. The rule is
whether the condition can be true for a page that is full at the size that
was requested: an emptiness test and a threshold at or below
`minimum_page_size` cannot be, a lower bound can only stop being satisfied
as the page shrinks, and an upper bound is safe only when it follows the
reduction. A shape the analysis cannot classify is warned about rather than
rejected, because this runs at stream construction and a false rejection
takes `check` and `discover` down with `read`. Reverting to the round-1
regex fails 12 of the new factory tests, in both directions.

`MAX_TOTAL_REDUCTIONS = 1000` guaranteed termination but moved the
`AFTER_SUCCESSFUL_PAGE` cliff from page 6 to page 1001 instead of removing
it, and the failure it produced was wrong three ways: `transient_error` on a
job that could never succeed, "the source kept failing" on a partition where
every page succeeded, and a page size that was restored rather than
requested. A stream that gets every page through after one reduction is
healthy and has to complete, so the cap is gone. `max_attempts` now bounds
only the reductions made in a row *without* a page succeeding, which is what
separates a stuck partition from an expensive one. Termination still holds:
only a successful page restarts the budget and `_read_pages` calls that once
per page it consumed, so every restart costs a page of progress. The
schema's `max_attempts` and `reset_policy` descriptions say so, and two
tests pin the healthy long stream and the stuck partition - the first one
fails against the previous revision.

Also:

- `PageSizeReductionRequiredException` goes back to `config_error`. On the
  SimpleRetriever path it is always caught and the type is inert; the only
  way it escapes is a retriever that never re-issues the page, where a
  job-level retry would fail identically forever. The escape path is kept
  deliberately - it is the only signal such a stream gets - and the message
  no longer claims a retry that will not happen.
- Two assertions passed vacuously. `"test" in internal_message` also matched
  the stream name, so it certified nothing about the error handler's
  `error_message` reaching the exception; it now asserts the mapping's text.
  `test_given_no_page_size_then_page_size_interpolates_to_none` held equally
  if `page_size` were never bound at all; it is now parametrized with two
  positive cases that pin the variable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…odel

Reverts 09882ec ("Auto-committed changes from Poe command `build`").

Running the codegen pipeline produced a +259/-265 diff against the committed
generated model and turned the MyPy Check red with 8 `valid-type` errors. Every
one is a `conint(...)` or `confloat(...)` used as an annotation, which mypy
rejects; the committed model on `main` contains none of these forms, so `main`
is green and only a regenerated file fails.

Three of the eight come from this PR (`PageSizeReduction.reduction_factor`,
`.minimum_page_size`, `.max_attempts`). The other five are pre-existing and
unrelated to this change: `DynamicStreamCheckConfig.stream_count`,
`ConstantBackoffStrategy` (x2), `ExponentialBackoffStrategy.jitter_range_in_seconds`,
and `AsyncRetriever.failed_retry_wait_time_in_seconds`. They are latent on every
branch and surface the moment anyone regenerates.

The root cause is in bin/generate_component_manifest_files.py: the
datamodel-codegen invocation does not pass `--field-constraints`, so numeric
`minimum` / `exclusiveMinimum` constraints become `conint()` / `confloat()`
instead of `Field(ge=...)`. Fixing that regenerates the whole file and should be
its own PR against main.

Reverting loses no validation. The manifest is checked against
declarative_component_schema.yaml by `jsonschema.validators.validate` in
`_validate_source()`, so the YAML bounds are enforced regardless of what the
generated model expresses - the same mechanism that enforces `minItems` on other
components, which codegen also drops.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stop-condition analysis read each comparison on its own, which is only
sound while that comparison decides the condition in its own polarity.
`{{ not (last_page_size >= 100) }}` and `{{ last_page_size - 100 < 0 }}` both
mean `last_page_size < 100`, yet both were accepted as SAFE, so a full page at
a reduced size read as a short page and the partition was truncated silently -
the exact failure the check exists to prevent.

A comparison is now classified only when it is reached from the root of the
expression through `and`/`or` alone, and the `lt`/`lteq` readings require
`last_page_size` to be compared bare rather than after arithmetic. Anything
else is UNKNOWN, which warns instead of rejecting.

Also brings the reducer's user-facing messages in line with the error-message
guideline: every message names the stream, and the two `transient_error`
messages drop the remediation the user cannot act on. The `config_error`
messages keep theirs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`{{ last_page_size < [page_size, 100] | max }}` was SAFE because the threshold
only had to *mention* `page_size`, while `max(50, 100)` is the configured size
again; `{{ last_page_size < page_size + 50 }}` had the same hole. The `lt`
branch now requires the threshold to be the `page_size` variable itself,
filters aside, so an expression built from it falls to UNKNOWN.

`{% if last_page_size < 100 %}true{% endif %}` had dropped from TRUNCATES to
UNKNOWN when the traversal started tracking whether a comparison decides the
condition. A `{% if %}` renders truthy text exactly when its test holds, so the
test is a decider - but only while the branch it guards is the whole story, so
an `else`, an `elif`, or a body the CDK reads as false keeps it unclassified.
Text rendered next to a comparison is unclassifiable for the same reason: the
condition is then truthy whatever the comparison decided.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…terminal failure message

Two fixes from the architecture review of this PR.

A `page_token_option` of type `RequestPath` makes the next page a URL the API
built, and that URL already carries the page size the API echoed back. The
reduced page size is injected as a request option on top of it, so the request
goes out with the page size twice and which one the API honors is up to the API:
every page after the first would keep asking for the size that just failed.
`ModelToComponentFactory` now rejects that combination at stream construction and
points at a `RequestOption` page token instead.

When the reduction bottoms out, the CDK only knows that the API rejected every
page size it asked for. What narrows a query down is API-specific, so
`PageSizeReduction` now takes an optional `failure_message` that is appended to
the two `transient_error` messages. It is deliberately not appended to the
`config_error` ones: those are about the manifest, not about the API rejecting a
page size, and they carry their own remediation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fe, and make the reduction budget mean what it says

Seven findings from the code review of this PR.

F1, the blocker. `classify_stop_condition` returned SAFE for every
`stop_condition` that did not name `last_page_size`, but the response body
carries the same count: `{{ response['data'] | length < 100 }}` is
`{{ last_page_size < 100 }}` counted one layer out, and it truncates in exactly
the same way. Which of a connector's own response fields holds a page length is
not knowable at config time, so a condition that bounds any value from above is
now reported as UNKNOWN - a warning, which is what this analysis already does
for every shape it cannot read. A condition that makes no upper-bound
comparison, the common `{{ not response.next }}` shape, cannot observe a page
length by comparison and stays SAFE. Measured over the monorepo at
`09124c5aabc`: of 1505 conditions, 1466 stay accepted, 3 rejected and 36 now
warn - the 36 are the response-derived page lengths in source-pardot,
source-zendesk-chat and source-zendesk-talk, which is the shape this is about.
All three of source-github's conditions stay accepted.

F2. `max_attempts` counts the reductions made in a row without a page getting
through, which is what the terminal error claims happened - but
`on_successful_page` only restarted the budget under `AFTER_SUCCESSFUL_PAGE`, so
under the default `NEVER` a partition in which every reduction was followed by a
successful page still failed at the `max_attempts + 1`-th reduction, blaming the
API for rejecting every page size it had in fact served. That is the same defect
as the `MAX_TOTAL_REDUCTIONS` cap this PR removed, with the cliff at the 6th
reduction instead of the 1001st page, and it is reachable exactly when per-page
cost varies - the GraphQL case the feature exists for. The budget now restarts
under both policies; `reset_policy` only decides whether the page size is
restored. Termination still holds: under `NEVER` the page size strictly
decreases, so `minimum_page_size` bounds the partition on its own.

F3. Each reduction divides the page size by `reduction_factor` and spends one
attempt, so an unbroken run of failures bottoms out at
`page_size / reduction_factor ** max_attempts` whatever `minimum_page_size`
says: with the defaults a floor of 10 on a page size of 1000 is never reached
and its error branch never fires. The factory now warns when the floor is out of
reach of the budget, and says how large `max_attempts` would have to be. It only
warns when the floor was set explicitly - the default of 1 is out of reach of
the default budget on any page size above 32 - and it does not raise, because
pages that succeed in between restart the budget, which makes the floor
reachable over a partition.

F4. The `PageSizeReduction` and `max_attempts` descriptions now say that the
wait between reductions is the CDK's own and does not consult the error
handler's `backoff_strategies` or a `Retry-After` header.

F5. `bin/generate_component_manifest_files.py` now records why
`declarative_component_schema.py` is edited by hand for bounded numeric fields:
without `--field-constraints` the generator emits `conint`/`confloat`
annotations that mypy rejects, on five fields that predate this PR.

F6. A rejected page appeared in the Connector Builder auxiliary panel titled
like an ordinary page fetch. `_as_auxiliary_request_log` now takes a title and
description, and the reduction path passes its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…only warn about a floor worth reaching

Two findings from the second round of review.

R2-F1. `_bounded_from_above` read `lt`/`lteq` in full - any non-literal left
operand counts as bounded - but read `gt`/`gteq` only when the threshold was a
literal, so `{{ config['page_size'] > response['data'] | length }}` stayed
classified as safe while `{{ response['data'] | length < config['page_size'] }}`
was warned about. Same comparison, same silent truncation, and the module
docstring promises that any comparison bounding a value from above is reported.
Every one of the four ordering operators bounds one of its operands from above
and only a literal is certain not to be a page length, so both directions are
now mirrored, as `_MIRRORED_OPERATORS` already does on the `last_page_size`
path. `{{ response.page >= response.total_pages }}` warns as a result: in
`a >= b` there is an upper bound on `b`, and the earlier reading of that shape
as "no upper bound, so nothing here can be a page length" was wrong.

Measured over the monorepo at `09124c5aabc`: of 1505 conditions, 1459 stay
accepted, 3 rejected and 43 warn, up from 36. The 7 newly warned are the two
shapes the mirror adds, source-serpstat x6 and source-jira x1; neither bounds a
page length, and neither connector opts into `page_size_reduction`. All three of
source-github's conditions, on the adopter branch, stay accepted.

R2-F2. The floor-reachability warning gated on `__fields_set__`, which records
that a value was supplied and not that it differs from the default, so spelling
`minimum_page_size: 1` out longhand earned a warning advising `max_attempts: 10`
on any page size above 32. It now gates on the floor being above 1, which is the
condition that makes the floor worth reaching in the first place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ries at the minimum page size

Two gaps found while adopting `page_size_reduction` in source-github, where GitHub answers 502
both to a GraphQL page that is too expensive and to a transient hiccup:

- The wait between attempts was a hard-coded 0.5s times the attempt count, so a run of five
  reductions spread six requests over ~7s. `backoff_seconds` makes it a knob; the default keeps
  today's behaviour.
- Once the page size reached `minimum_page_size`, the first response asking for a smaller page
  ended the stream. `REDUCE_PAGE_SIZE` bypasses the HTTP retry budget, so at the floor a stream
  got fewer attempts than it had before adopting the reduction. `retries_at_minimum_page_size`
  re-issues the same page unchanged, after the backoff, in a budget of its own, separate from
  `max_attempts` (which only counts reductions) and restarted by every successful page. The
  default of 0 keeps today's behaviour.

The `config_error` raised when the page size can never be reduced is now limited to the case
where `minimum_page_size` is what blocks it, which is the only one the user can act on. A stream
whose configured page size is already 1 gets the transient error instead: one record per page is
as small as a page gets, so the API rejecting it says nothing about the configuration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`PageSizeReducer` took `sleep: Callable = time.sleep`, which binds the function object when
the module is imported. A connector test that patches `time.sleep` to keep a run of reductions
from taking its backoff for real therefore had no effect, and a suite covering a reduction down
to the minimum page size paid the whole wait. The override is now stored and `time.sleep` is
looked up per call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…page size got there

Two findings from the fourth round of review.

R4-F1. `_retry_at_minimum_page_size` tested the `config_error` branch first, and
that branch never consulted `retries_at_minimum_page_size`. So the same manifest
gave a partition its retries when the reduction walked down to the floor and
none at all when the user's `page_size` was already there: the first response
ended the stream, telling the user to raise `page_size` or lower
`minimum_page_size` - a manifest field they cannot reach, for a case the
connector author had already answered by configuring retries. Whether a
transient error was retried therefore depended on how the page size arrived at
the floor rather than on anything about the error.

The retries now come first and the misconfiguration is reported once they are
spent, so it is still reported and still names both numbers to change. Measured
over the eight configurations that reach the floor: `configured=20, min=10,
retries=3` and `configured=10, min=10, retries=3` now take the same three waits
and differ only in the failure type, which is the one thing that should differ -
a floor that blocks every reduction is the connector's to fix, a floor the
reduction reached means the API kept rejecting every size.

R4-F2. The sleep-resolution fix of `01add3fa` was not pinned by any test: every
test here passes its own `sleep`, so none exercised the default path.
`test_given_no_sleep_override_when_reduce_then_wait_through_time_sleep` patches
`page_size_reducer.time.sleep` and asserts the wait lands there. Verified
against the defect: restoring the bound default makes that test fail, and the
file's run takes 7.5s of real sleeping instead of 0.45s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

Testing This CDK Version

You can test this version of the CDK using the following:

# Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@tolik0/cdk/reduce-page-size-request-path#egg=airbyte-python-cdk[dev]' --help

# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch tolik0/cdk/reduce-page-size-request-path

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /autofix - Fixes most formatting and linting issues
  • /poetry-lock - Updates poetry.lock file
  • /test - Runs connector tests with the updated CDK
  • /prerelease - Triggers a prerelease publish with default arguments
  • /poe build - Regenerate git-committed build artifacts, such as the pydantic models which are generated from the manifest JSON schema in YAML.
  • /poe <command> - Runs any poe command in the CDK environment
📚 Show Repo Guidance

Helpful Resources

📝 Edit this welcome message.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

PyTest Results (Fast)

4 609 tests   - 65   4 597 ✅  - 65   7m 45s ⏱️ - 1m 19s
    1 suites ± 0      12 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit e852c7d. ± Comparison against base commit a17b5a3.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

PyTest Results (Full)

4 612 tests   - 65   4 600 ✅  - 65   13m 36s ⏱️ -53s
    1 suites ± 0      12 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit e852c7d. ± Comparison against base commit a17b5a3.

♻️ This comment has been updated with latest results.

…nd page_size

`page_size` is bound to the page size that was actually requested, which is what makes
`{{ last_page_size < page_size }}` the one comparison that survives a reduction. But it is
only a number when the CursorPagination strategy declares a `page_size`. Without one it
binds to `None`, and the comparison does not fail: Jinja raises, `JinjaInterpolation._eval`
treats the TypeError as "not a template" and returns the raw template string, and
`InterpolatedBoolean` reads a non-empty string as `True`. The stop condition is then
satisfied on page 1 and the rest of the partition is dropped without anything failing.

This mattered beyond the streams that reduce: the example and the advice to prefer
`page_size` over a hardcoded number sit on `CursorPagination.stop_condition`, a field 710
pagination blocks across 97 connectors already use without declaring a `page_size`.

`CursorPaginationStrategy.__post_init__` now rejects a `stop_condition` or `cursor_value`
that reads `page_size` while the strategy declares none. The reference is read off the
Jinja AST, so the bare variable is told apart from `config['page_size']`, which is a
lookup on the config and is bound either way. Rejecting at construction rather than
failing per page keeps a sync from emitting a truncated partition first.

The schema now says the variable needs a declared `page_size`, and points at
`{{ last_page_size == 0 }}` for the case where there is none.

No manifest in the monorepo is affected: 0 of 535 declare a CursorPagination without a
`page_size` whose `stop_condition` or `cursor_value` reads the variable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… page token URL

`page_size_reduction` rejected a `page_token_option` of type RequestPath: the next page is then a
full URL built by the API which already carries the page size it echoed back, so the reduced page
size was injected next to the original one and the request went out asking for both.
`HttpClient._dedupe_query_params` only drops the injected parameter when the two values agree, which
is exactly when there is nothing to reduce.

`DefaultPaginator.path` now takes the reduced page size and rewrites that parameter inside the token
URL, leaving a single page size in the request. Only the parameter named by `page_size_option` is
touched and only when the URL already carries it; every other pair is kept byte for byte rather than
parsed and re-encoded, so a token URL the API built is handed back as it was written.

Whether a page token URL can be re-requested at a smaller page size at all is something only the
connector knows - it holds for a URL addressing records by cursor or timestamp, and not for one
carrying a page number, where a smaller page size moves every following boundary and skips records
the way PageIncrement does. A RequestPath token is opaque to the CDK, so this is opted into with
`rewrite_page_size_in_page_token_url` rather than assumed, and the manifest is rejected when the
field is set without a RequestPath token or with a page size that does not inject into a request
parameter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ion for RequestPath

The PageSizeReduction description was updated when rewrite_page_size_in_page_token_url was added, but
the field description on SimpleRetriever still said a RequestPath page token is rejected outright.

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

This branch was successfully deployed

2 active (outdated) deployments
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