Pewarta Proxy

📋 Server Request & Proxy Logs

Loading active proxy settings...
Developed by Khairul Adha (adhexa.id)
Mengambil log server...

⚙️ Pengaturan Proxy Target

Skip to content

feat(declarative): support RequestPath tokens and first-request checks in SyncTokenCursor - #1238

Draft
devin-ai-integration[bot] wants to merge 3 commits into
devin/1792189821-opaque-token-resetfrom
devin/1792471568-sync-token-request-path
Draft

devin-ai-integration[bot] wants to merge 3 commits into
devin/1792189821-opaque-token-resetfrom
devin/1792471568-sync-token-request-path

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Overview

👉 TL;DR: Lets the sync-token incremental cursor replay a stored token that is a complete URL (Microsoft Graph delta links), and adds build-time checks so first-request-only token shapes (Box events) fail with a clear message instead of a request collision on page 2.

Specifically, SyncTokenCursor.token_option now accepts RequestPath, SimpleRetriever gains a first_request_path hook with an origin check, the factory validates token/paginator collisions and inject_on_first_request, and SyncTokenCursor caps stored token state at 4 MB per stream.

Resolves https://github.com/airbytehq/airbyte-internal-issues/issues/18023:

Close on merge: yes — internally filed, no customer to confirm with.

This PR is part of a stacked set:

Changes

  • declarative_component_schema.yaml / model: SyncTokenCursor.token_option is anyOf: [RequestOption, RequestPath] (Union[RequestOption, RequestPath]).
  • SyncTokenRequestOptionsProvider: returns {} from all request-option methods for RequestPath; new get_request_path(stream_slice) returns the stored token, else rendered initial_token, else None.
  • SimpleRetriever.first_request_path: on the first request of a pass (no paginator path, no page token) the token URL replaces the requester path; paginator RequestPath links still win on later pages. Absolute token URLs whose scheme or host differ from the requester base raise a config_error before any request is sent.
  • ModelToComponentFactory:
    • binds first_request_path to the inner SyncTokenRequestOptionsProvider (unwrapping PerPartitionRequestOptionsProvider) only for SyncTokenCursor + RequestPath; every other stream gets None.
    • after building the retriever, compares the resolved destinations (RequestOption.resolve_target(config), so field_name: x equals field_path: [x]) of the RequestOption token and the paginator options. It rejects an injected page_size_option on the same field even with the ignore flag; a page_token_option on the same field when the paginator sends a token on the first request; and otherwise a page_token_option on the same field unless ignore_stream_slicer_parameters_on_paginated_requests: true. If a destination can't be resolved at build time, the runtime collision check still applies.
    • rejects PageIncrement/OffsetIncrement with inject_on_first_request: true when the token goes in the path or on the first request only.
  • SyncTokenCursor: max_state_bytes (default 4 MB) tracked incrementally from partition key + token sizes; stored partitions keep updating, new partitions past the limit are read in full, one WARN per stream.

Review Spotlight

Reviewers with limited time, please review first:

Reproduction

On the base branch (4ced6454):

  • SyncTokenRequestOptionsProvider(token_option=RequestPath(...)).get_request_params(...) → AttributeError: 'RequestPath' object has no attribute 'inject_into'.
  • Box shape (token and paginator both stream_position as request parameters) → page 2 fails with ValueError: Request body collision, duplicate keys detected at key path: stream_position.
  • The new tests: 31 failed / 7 passed before the fix.

Risks

  • Reset (RESET_SYNC_TOKEN) with a RequestPath token re-reads from initial_token if set, otherwise the requester path — same as the RequestOption patterns. A manifest whose initial_token means "sync from now" would skip history on reset; adopters should leave initial_token unset for Graph.
  • The origin check covers the stored token URL only, not paginator next links.
  • The two new factory checks only apply to SyncTokenCursor streams, which are unreleased (this stack).

Declarative-First Evaluation

This is a CDK change that adds built-in declarative capability; no connector custom component is written. The adopters (Graph, Box) are manifest-only and could not express this with existing components: DefaultPaginator with RequestPath only applies after a page token exists (not the first request), RequestOption cannot carry a full URL, and no existing check catches the token/paginator key collision at build time.

Test Coverage

New tests in test_sync_token_cursor_integration.py (exact URLs/params via HttpMocker), test_sync_token_cursor.py, and test_sync_token_request_options_provider.py: T-URL, T-URL-PARAMS, T-URL-INIT, T-URL-RESET, T-URL-PAR, T-ORIGIN, T-POS, T-COLLIDE, T-FIRSTREQ, T-BYTES, T-INERT, resolved-destination collision cases (field name vs path, interpolated and nested paths, page size with and without the ignore flag), plus provider and RequestOption.resolve_target unit tests.

  • Focused declarative suites: 1173 passed (after review fixes in 6f91913b: 1058 passed across the cursor, requester, and parser suites). Full unit_tests/: 5820 passed, 22 skipped, 2 failed — test_parse_start_date[date_only_ab_datetime_parse_fallback] and [with_timezone_offset_converted_to_utc], which fail the same way on the base commit 4ced6454.
  • ruff check, ruff format --check, mypy on changed files: clean.

Versioning

No breaking change: existing RequestOption manifests and the stored state format are unchanged, and RequestPath is an added union member. The CDK version comes from the release process via the conventional feat title; no version file is edited.

Follow-ups

  • Docs: docs/platform/connector-development/config-based/understanding-the-yaml-file/incremental-syncs.md in airbytehq/airbyte has no SyncTokenCursor section yet (PRs 1234/1235 did not add one), so the RequestPath / first-request guidance from the issue should land with the stack's docs PR.

Link to Devin session: https://app.devin.ai/sessions/ef74cd18b819430c89b904a53a3ed78e
Open in Devin Desktop: https://app.devin.ai/desktop/session/ef74cd18b819430c89b904a53a3ed78e?variant=devin

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@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@devin/1792471568-sync-token-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 devin/1792471568-sync-token-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 Oct 20, 2026 •

Copy link
Copy Markdown

PyTest Results (Full)

6 110 tests  +97   6 079 ✅ +97   11m 42s ⏱️ + 4m 1s
    1 suites ± 0      31 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit 2890216. ± Comparison against base commit 7789d58.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

PyTest Results (Fast)

6 107 tests  +97   6 076 ✅ +97   6m 40s ⏱️ +38s
    1 suites ± 0      31 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit 2890216. ± Comparison against base commit 7789d58.

♻️ This comment has been updated with latest results.

@pnilan

Patrick Nilan (pnilan) commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

/prerelease

Prerelease Job Info

This job triggers the publish workflow with default arguments to create a prerelease.

Prerelease job started... Check job output.

✅ Prerelease workflow triggered successfully.

View the publish workflow run: https://github.com/airbytehq/airbyte-python-cdk/actions/runs/39463931382

@pnilan

Copy link
Copy Markdown
Member

🤖 This comment was generated by an AI Agent.

Review of e09fd862d5c556f47d0f21fcba446693aca9ee89 against the stacked base 4ced6454cba98c2f18ffc271fe31594c6bd2663d: two P2 findings in the new collision validator. Please address both and add the regression coverage below.

1. [P2] Compare resolved option destinations, not raw template strings

Location: model_to_component_factory.py:2354–2361.

The equality checks can reject a working manifest. Give both options this field expression:

{{ parameters.get('target', 'nextCursor') }}

Then set incremental_sync.$parameters.target: syncToken. The cursor's option resolves to syncToken, while the paginator's option resolves to nextCursor. These are distinct destinations, and the base produces the expected request body:

{"syncToken": "STORED", "nextCursor": "NEXT"}

This PR rejects the manifest before the request because the two expression strings are identical. The first regression test below passes on the base and fails on the PR head.

The reverse gap exists too: a token option with field_name: syncToken and a paginator option with field_path: [syncToken] pass the new validator, then collide at runtime. This second case is incomplete coverage in the new validation; the runtime collision itself also existed before this PR.

Code recommendation:

  • Keep structural checks in _validate_sync_token_stream, but move destination comparison to a construction stage where the actual RequestOption components and config are available. create_simple_retriever already has the unwrapped token provider and constructs the paginator.
  • Reuse those constructed options so validation uses exactly the same interpolation context as request generation. Do not render both with a shared parameter dictionary or assume the option model's own $parameters is the context: create_request_option uses the parameters argument passed by its caller, and the cursor and paginator construction paths differ.
  • Normalize the effective destination to (inject_into, tuple_of_path_segments). A scalar field name syncToken and a one-element field path [syncToken] must produce the same target; preserve nested path segments instead of comparing a dot-joined string.
  • A way to avoid duplicating request-option rendering rules is to call the constructed option's inject_into_request on a scratch mapping with a unique sentinel value and the real config, then recover the sentinel's key path. Alternatively, extract/reuse a field-resolution helper from RequestOption. Neither approach needs an HTTP request.
  • If a destination cannot be resolved reliably at construction time, do not infer a collision from identical template text; retain the runtime collision check for that case.

2. [P2] Validate active page-size collisions independently of the ignore flag

Location: model_to_component_factory.py:2345–2348 and the remediation at lines 2364–2368.

The new check treats page_token_option and page_size_option alike and recommends ignore_stream_slicer_parameters_on_paginated_requests: true for either conflict. That flag cannot fix a page-size conflict: the paginator sends its page size on the first request, when the stored token is also sent.

Reproduced with CursorPagination.page_size: 100, both page_size_option and token_option targeting body_json.syncToken, and a stored token of STORED. With the recommended flag enabled, construction succeeds, but request one attempts to merge {"syncToken": 100} with {"syncToken": "STORED"} and raises the same collision error. The new remediation therefore sends the author to a configuration that still fails.

Code recommendation: separate the two checks. The following is control-flow pseudocode; resolved_target represents the helper described above:

token_target = resolved_target(token_option, config)

# Only check an active page-size option that actually injects a page size.
if page_size_is_injected and same_known_target(
    token_target, resolved_target(page_size_option, config)
):
    raise ValueError(
        f"Stream {stream_name}: token_option and page_size_option target the "
        "same request field on the first request. Configure distinct fields; "
        "ignore_stream_slicer_parameters_on_paginated_requests cannot fix this."
    )

if same_known_target(token_target, resolved_target(page_token_option, config)):
    if page_token_is_injected_on_first_request:
        # The ignore flag cannot separate these values on request one either.
        raise ValueError(...)
    if not ignore_stream_slicer_parameters:
        # Here the existing first-request-only recommendation is applicable.
        raise ValueError(...)

Keep the existing RequestPath/first-request paginator checks. Preserve the valid Box case: a page-token collision with the ignore flag enabled and no paginator token on request one must still build and sync successfully. Also avoid rejecting an unused page_size_option when no page size is injected.

Reproduction and regression tests

These tests use the existing helpers in test_sync_token_cursor_integration.py; add them there. All three expected-behavior assertions fail on the reviewed head. The distinct-rendered-fields test passes on the stacked base.

def _review_retriever(manifest):
    source, _ = _source_and_catalog(manifest)
    stream = source.streams(config={})[0]
    return stream._stream_partition_generator._partition_factory._retriever


def test_distinct_rendered_fields_are_allowed():
    from airbyte_cdk.sources.types import StreamSlice

    manifest = _manifest()
    stream = manifest["streams"][0]
    options = (
        stream["incremental_sync"]["token_option"],
        stream["retriever"]["paginator"]["page_token_option"],
    )
    for option in options:
        option["field_name"] = "{{ parameters.get('target', 'nextCursor') }}"
    stream["incremental_sync"]["$parameters"] = {"target": "syncToken"}

    retriever = _review_retriever(manifest)
    assert retriever._request_body_json(
        stream_slice=StreamSlice(partition={}, cursor_slice={"token": "STORED"}),
        next_page_token={"next_page_token": "NEXT"},
    ) == {"syncToken": "STORED", "nextCursor": "NEXT"}


def test_page_size_collision_is_rejected_even_with_ignore_flag():
    manifest = _manifest()
    retriever_model = manifest["streams"][0]["retriever"]
    retriever_model["ignore_stream_slicer_parameters_on_paginated_requests"] = True
    paginator = retriever_model["paginator"]
    paginator["page_size_option"] = {
        "type": "RequestOption",
        "inject_into": "body_json",
        "field_name": "syncToken",
    }
    paginator["pagination_strategy"]["page_size"] = 100

    with pytest.raises(ValueError):
        _review_retriever(manifest)


def test_equivalent_field_name_and_path_are_rejected():
    manifest = _manifest()
    paginator = manifest["streams"][0]["retriever"]["paginator"]
    paginator["page_token_option"] = {
        "type": "RequestOption",
        "inject_into": "body_json",
        "field_path": ["syncToken"],
    }
    with pytest.raises(ValueError):
        _review_retriever(manifest)

For the final tests, assert the relevant error text as well as ValueError. Extend coverage to both directions of the field_name/field_path comparison, distinct expressions that resolve to the same field, nested JSON paths, page-size conflicts with the flag both off and on, and nonconflicting options. Keep the existing Box success test green.

Suggested change scope:

airbyte_cdk/sources/declarative/
  parsers/model_to_component_factory.py       — resolve destinations; separate validation rules
  requesters/request_option.py               — only if extracting a shared resolution helper
unit_tests/sources/declarative/incremental/
  test_sync_token_cursor_integration.py       — add the regression cases above

No state-format, cursor commit/reset, URL replay, or schema changes are needed for these findings.

Validation already performed: 85 focused tests passed across the sync-token cursor, integration, and request-options-provider suites. The three additional reproduction cases fail on the PR head; the valid-template case passes on the base. The MotherDuck CI failure is unrelated: its job cannot retrieve an enabled credential-secret version.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

👍 On it. Both findings are valid. Here's the plan:

  1. Remove the raw-template comparison from _validate_sync_token_stream. Collisions will instead be checked in create_simple_retriever, using the constructed RequestOption components and the real config. A new RequestOption.resolve_target will find each option's actual destination by injecting a sentinel value, then compare (inject_into, path segments). That way syncToken and [syncToken] count as the same target, and nested paths keep their segments. If a destination can't be resolved at build time, the check is skipped and the runtime collision check still applies.
  2. Split the rules. A page-size collision (when a page size is actually injected) is rejected even when ignore_stream_slicer_parameters_on_paginated_requests is on. A page-token collision is rejected when the paginator sends a token on the first request. Otherwise, a page-token collision without the flag gets the existing first-request-only message. The Box case (flag on, no first-request page token) still builds.

I'll add your three tests, plus the extra cases you listed, with error-text assertions.


Posted by Devin, as requested by Patrick Nilan (@pnilan).

…fields

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

☑️ Resolved in 6f91913. Collisions are now checked against the resolved destinations of the constructed options, and the page-size and page-token rules are separate.

  • RequestOption.resolve_target(config) returns the key path that inject_into_request actually writes, so field_name: syncToken and field_path: [syncToken] count as the same target and nested segments are kept. The factory compares (inject_into, path) after it builds the retriever. If a destination can't be resolved at build time, the check is skipped and the runtime check still applies.
  • An injected page_size_option on the token's field is rejected even when the ignore flag is on. A page_token_option on the token's field is rejected when the paginator sends a token on the first request. Otherwise it's rejected only when the ignore flag is off. The Box configuration still builds, and the T-FIRSTREQ check is unchanged.
  • Tests: your three cases, both field-name/path directions, different expressions that resolve to the same field, nested paths, page size with the flag on and off, an unused page-size option, and nonconflicting options. Error text is asserted with match=. Your three tests fail on e09fd862 and pass now.

Posted by Devin, as requested by Patrick Nilan (@pnilan).

@pnilan
Patrick Nilan (pnilan) marked this pull request as ready for review October 28, 2026 16:53
@pnilan
Patrick Nilan (pnilan) requested a review from a team as a code owner October 28, 2026 16:53
…set' into devin/1792471568-sync-token-request-path

Bring in the current base (#1235, which includes #1234's review fixes) and resolve conflicts:
- model_to_component_factory.py: keep both new static methods,
  _validate_sync_token_option_collisions (this PR) and
  _flatten_partition_router_models (base).
- sync_token_request_options_provider.py: keep this PR's RequestPath support and
  _get_token helper; take the base's docstring, its removal of the dead
  `not self.token_option` check, and valid_types=(str,) for initial_token.
- sync_token_cursor.py: say "sync token" in the log line of this PR's new
  first-commit branch, matching the base's rename.
  test_commit_observability_counts_completed_pages_per_partition fails otherwise.

This branch was successfully deployed

1 active (outdated) and 1 inactive (outdated) deployments
PyPi — e09fd862 Deployed Oct 20, 2026 by octavia-bot[bot] via Publish CDK version to PyPI #332
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant