Skip to content

fix: user-facing duplicate handling workflow for bucket-based connectors (backport of #2077)#2096

Merged
edwinjosechittilappilly merged 7 commits into
mainfrom
backport/2077-duplicate-handling-bucket-connectors
Jul 16, 2026
Merged

fix: user-facing duplicate handling workflow for bucket-based connectors (backport of #2077)#2096
edwinjosechittilappilly merged 7 commits into
mainfrom
backport/2077-duplicate-handling-bucket-connectors

Conversation

@edwinjosechittilappilly

@edwinjosechittilappilly edwinjosechittilappilly commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Backports #2077 from release-saas-ga-0.6.2 to main.

Depends on #2095 (backport of #2066), #2094 (#2050), #2093 (#2035), #2092 (#2025) — stacked because it substantially rewrites the ingest flow in frontend/components/file-browser-dialog.tsx and frontend/components/connectors/shared-bucket-view.tsx, both touched by earlier backports. Merge order: #2092#2093#2094#2095 → this PR.

Note on conflict resolution: main has an onIngestSuccess callback prop on FileBrowserDialog (added independently of the release branch) that release's version of this file doesn't have. Resolved by taking release's file (duplicate-check flow) as the base and re-threading onIngestSuccess through submitSync so it still fires with the mutation result on successful ingest, matching its original main behavior. Verified with tsc --noEmit — no errors in either touched file.

Test plan

  • CI passes
  • Manually verify: ingesting files with existing duplicates in a bucket connector shows the duplicate-handling dialog (skip/overwrite), and onIngestSuccess still fires (task tracking / invalidation) on success

Summary by CodeRabbit

  • New Features
    • Added duplicate detection before ingesting files and bucket selections.
    • Users can overwrite duplicates or skip them during syncing.
    • Added a duplicate-handling dialog with duplicate vs non-duplicate counts and names.
    • Added shared-ingest options that appear only when appropriate, affecting what gets sent during ingest.
    • Improved ingest UX by reflecting duplicate-check progress in button state/label.
  • Bug Fixes
    • Improved OpenSearch compatibility by retrying lookups when index mappings differ.
    • Added clearer handling when all selected items are already imported (with informative messaging).
  • UX Improvements
    • Expanded selection behavior to allow re-ingesting already imported files.

edwinjosechittilappilly and others added 5 commits July 14, 2026 14:38
…hare all" toggle (#2025)

* feature Flag Share all

* style: apply biome auto-fixes [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix: COS shared-toggle owner not propagated on bucket sync + stale on dedupe skip

Two related bugs kept "Make documents available to all users" from actually
sharing documents:

1. connector_sync's bucket_filter branch (the path the COS connector UI's
   "Ingest N Buckets" button always hits) never forwarded body.shared to
   either of its two sync_specific_files calls (new files / changed files),
   so newly-ingested files always ended up privately owned regardless of the
   toggle.

2. ConnectorFileProcessor.process_item's duplicate-filename and
   unchanged-hash skip branches returned before resolve_shared_owner_fields()
   ever ran, so re-syncing a bucket whose files were already indexed left
   their ownership exactly as it was on the prior sync.

Fix (2) via _reconcile_shared_owner(): a metadata-only update_by_query
scoped to owner==self OR ownerless (same boundary as
delete_document_by_filename), called from both skip paths instead of
silently leaving stale ownership in place.

Also fixes two pre-existing test_shared_flag.py tests that had drifted from
connector_sync's current signature (missing request/session/rbac args).

* test: guard is_dev_ibm_cos_enabled's default against re-regressing to enabled

Found during review: is_dev_ibm_cos_enabled() previously defaulted
OPENRAG_DEV_IBM_COS to "true" when unset (now fixed to "false", matching
is_dev_azure_blob_enabled's pattern), which made the Enterprise/SaaS-only
IBM COS connector available in every deployment by default. The existing
tests only monkeypatched the function away, so they wouldn't have caught
this. Add a test asserting the unset-env-var default.

* style: ruff autofix (auto)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Root cause: FileBrowserDialog (the "Browse Files" individual-file picker) never read the "Make documents available to all users" toggle or ingest settings — it built its sync request with only selected_files, so shared silently defaulted to false on the backend. The whole-bucket ("folder") ingest path in SharedBucketView already did this correctly.

Changes (frontend-only, no backend changes needed — it already threads shared uniformly):
- frontend/components/file-browser-dialog.tsx: added showShared/ingestSettings props, and now includes settings and shared in the sync request body, mirroring the bucket-ingest path.
- frontend/components/connectors/shared-bucket-view.tsx: passes its existing showSharedToggle/ingestSettings state down into FileBrowserDialog.
…based connectors (#2077)

* fix japanese file names

Root cause: COS/Azure/S3 connectors build document.id as the raw, non-ASCII object key ("bucket::報告書.pdf"), and that value was reused verbatim as document_id in the default Langflow ingestion path — the only ingestion path that puts non-ASCII text into ASCII-only HTTP headers and an unbounded-length OpenSearch identifier. Google Drive (opaque ASCII ID) and manual upload (content hash) were structurally immune; COS/Azure/S3 were the only paths exposed.

Fix applied (matches the already-proven split used by the non-Langflow ingestion path):
- document_index_writer.py — DocumentIndexContext gained a connector_file_id field, now written onto every indexed chunk when present.
- langflow_file_service.py — _resolve_document_id now hashes any supplied connector_file_id into a stable ASCII document_id, instead of using it verbatim; connector_file_id threads through run_ingestion_flow → _configure_ingest_callback → the JWT context.
- processors.py — the Langflow-path connector branch now passes connector_file_id=document.id (not document_id=document.id), and the post-ingest existence check / metadata sync now match on connector_file_id.
- connectors/service.py — stale comments corrected (dedupe/ACL-sync already matched both fields, no logic change needed there).
- Added 13 new unit tests (test_resolve_document_id_connector_file_id.py, test_document_index_writer_connector_file_id.py, test_bucket_connector_unicode_file_ids.py) — confirmed they fail on the old code and pass on the fix.
- Ran the full unit suite: same 14 pre-existing failures before and after (unrelated to this change — auth/settings/encryption tests), no new regressions.

* style: ruff autofix (auto)

* fix connector File name issue

* ingest fix

* Update processors.py

* review fix

* fix connector dialog on rewrite

* overwrite dialog box

* Update connectors.py

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
@github-actions github-actions Bot added frontend 🟨 Issues related to the UI/UX backend 🔷 Issues related to backend services (OpenSearch, Langflow, APIs) tests bug 🔴 Something isn't working. labels Jul 14, 2026
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8ce85040-ebc3-4eb2-a4d1-13bacadbc5c1

📥 Commits

Reviewing files that changed from the base of the PR and between 2104693 and f8b54f1.

📒 Files selected for processing (1)
  • src/api/connectors.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/api/connectors.py

Walkthrough

Connector ingestion now performs duplicate pre-checks for bucket and file selections, supports overwrite or skip handling, propagates shared-ingest settings, queues sync tasks, and retries incompatible OpenSearch aggregations using keyword fields.

Changes

Connector ingestion updates

Layer / File(s) Summary
Duplicate classification and aggregation contracts
src/api/connectors.py, tests/unit/api/test_connector_change_detection.py, tests/unit/connectors/test_connector_file_type_validation.py
Duplicate checks support bucket filters and classify files by ingested connector-file existence. OpenSearch aggregation helpers retry with keyword subfields when analyzed text fields fail, with unit coverage.
Shared bucket duplicate workflow
frontend/components/connectors/shared-bucket-view.tsx
Shared bucket ingestion checks duplicates, conditionally displays shared settings, supports overwrite or skip actions, queues returned tasks, and updates ingest loading states.
File browser duplicate workflow
frontend/components/file-browser-dialog.tsx
File browser ingestion checks duplicates before syncing, permits already-ingested file selection, and submits overwrite or skip payloads through the sync mutation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SharedBucketView
  participant connector_check_duplicates
  participant DuplicateHandlingDialog
  participant SyncMutation
  User->>SharedBucketView: Start ingestion
  SharedBucketView->>connector_check_duplicates: Submit selected files or bucket filter
  connector_check_duplicates-->>SharedBucketView: Return duplicate and non-duplicate files
  SharedBucketView->>DuplicateHandlingDialog: Show duplicate choices
  User->>DuplicateHandlingDialog: Choose overwrite or skip
  DuplicateHandlingDialog->>SyncMutation: Submit selected files with replace_duplicates
  SyncMutation-->>SharedBucketView: Return task_ids
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: mfortman11, wallgau, lucaseduoli

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: a user-facing duplicate handling workflow for bucket-based connectors.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backport/2077-duplicate-handling-bucket-connectors

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

React Doctor found 1 new issue in 1 file · 1 warning · score 84 / 100 (Needs work) · 0 fixed · vs main

1 warning

components/file-browser-dialog.tsx

  • ⚠️ L53 Large component is hard to read and change no-giant-component

Reviewed by React Doctor for commit f8b54f1. See inline comments for fixes.

ingestSettings?: IngestSettings;
}

export function FileBrowserDialog({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

React Doctor · react-doctor/no-giant-component (warning)

Component "FileBrowserDialog" is 363 lines long, which is hard to read & change. Split it into a few smaller components.

Fix → Pull each section into its own component so the parent is easier to read, test, and change.

Docs

@github-actions github-actions Bot added bug 🔴 Something isn't working. and removed bug 🔴 Something isn't working. labels Jul 14, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
enhancements/connectors/ibm_cos/connector.py (1)

71-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Refactor availability gating to use run-mode helpers.

Based on learnings, the repository is migrating away from the legacy IBM_AUTH_ENABLED flag for Enterprise/SaaS gating. You should use the run-mode helpers from utils.run_mode_utils instead, updating both the implementation and its tests.

  • enhancements/connectors/ibm_cos/connector.py#L71-L74: replace IBM_AUTH_ENABLED with is_run_mode_saas() or is_run_mode_on_prem() in the is_available check.
  • tests/unit/connectors/test_ibm_cos_connector.py#L20-L31: update the monkeypatches to mock is_run_mode_saas and is_run_mode_on_prem instead of IBM_AUTH_ENABLED.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@enhancements/connectors/ibm_cos/connector.py` around lines 71 - 74, The
is_available gating in enhancements/connectors/ibm_cos/connector.py#L71-L74 must
stop using IBM_AUTH_ENABLED and instead allow availability when
is_run_mode_saas() or is_run_mode_on_prem() is true, while preserving the
development bypass. Update the corresponding monkeypatches in
tests/unit/connectors/test_ibm_cos_connector.py#L20-L31 to mock is_run_mode_saas
and is_run_mode_on_prem rather than IBM_AUTH_ENABLED.

Source: Learnings

frontend/components/cloud-picker/ingest-settings.tsx (1)

232-246: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove commented-out dead code.

This Table Structure section is commented out. Removing it will keep the file clean.

♻️ Proposed refactor
-            {/* <div className="flex gap-2 items-center justify-between">
-              <div>
-                <div className="text-sm font-semibold pb-2">Table Structure</div>
-                <div className="text-sm text-muted-foreground">
-                  Capture table structure during ingest.
-                </div>
-              </div>
-              <Switch
-                id="table-structure"
-                checked={currentSettings.tableStructure}
-                onCheckedChange={(checked) =>
-                  handleSettingsChange({ tableStructure: checked })
-                }
-              />
-            </div> */}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/cloud-picker/ingest-settings.tsx` around lines 232 - 246,
Remove the commented-out Table Structure JSX block near the ingest settings UI,
including its wrapper, descriptive text, Switch, and handlers; leave the
surrounding active UI unchanged.
src/models/processors.py (1)

839-890: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Per-file update_by_query on every duplicate/unchanged skip adds write load on every routine re-sync.

_reconcile_shared_owner unconditionally issues a synchronous update_by_query per filename alias on every file that hits the skip path — which is the common case for a periodic re-sync of an already-fully-ingested bucket. Previously this path was pure read-only; now every unchanged file in a large bucket adds a write (and its OpenSearch-side cost) even when the shared setting never changed between syncs. For connectors with large, frequently-resynced buckets this could add meaningful sustained write load.

Consider either gating this behind a per-sync "has shared changed since last run" check, batching it into a single update_by_query per sync run (scoped by connector_type + user_id) instead of per file, or at least passing wait_for_completion=False to avoid blocking process_item on the write.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/models/processors.py` around lines 839 - 890, Reduce write overhead from
_reconcile_shared_owner by avoiding synchronous per-alias update_by_query calls
on every unchanged or duplicate skip. Prefer batching reconciliation once per
sync, scoped to the connector and user, or gate it on detecting that the shared
setting changed since the previous run; if retaining per-file updates, make them
asynchronous with wait_for_completion=False and ensure process_item is not
blocked.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/components/connectors/shared-bucket-view.tsx`:
- Around line 198-217: The sync request currently sends connection_id from
shared-bucket-view.tsx, but ConnectorSyncBody and the backend sync handler do
not preserve or use it, while file-browser-dialog.tsx omits it. Add
connection_id to ConnectorSyncBody, send the selected connection ID from both
runSelectedFilesSync flows in shared-bucket-view.tsx and
file-browser-dialog.tsx, and update connector_sync to reuse that ID for
duplicate checks and ingest instead of falling back to the first working
connection.

In `@src/api/connectors.py`:
- Around line 774-789: Update the docstring of
_classify_bucket_connector_duplicates to describe existence-only classification:
treat already-existing remote blobs as duplicates and unseen blobs as
non-duplicates, without comparing modified_time or claiming to mirror
modified-time reconciliation. Keep the documented handling of duplicate and new
blobs consistent with the function’s actual return behavior.
- Around line 790-802: Update the file-listing flow around connector.list_files
so it does not mutate the shared connector.bucket_names returned by
connection_manager.get_connector(). Pass bucket_filter through the list_files
API for each paginated request, or create a per-request connector copy, while
preserving pagination and filtering behavior.
- Around line 78-86: Update _is_unmapped_keyword_agg_error to return true only
when the error message contains the exact “Text fields are not optimised”
mapping-error text; remove the broad fielddata substring check so unrelated
fielddata failures are propagated normally.

---

Nitpick comments:
In `@enhancements/connectors/ibm_cos/connector.py`:
- Around line 71-74: The is_available gating in
enhancements/connectors/ibm_cos/connector.py#L71-L74 must stop using
IBM_AUTH_ENABLED and instead allow availability when is_run_mode_saas() or
is_run_mode_on_prem() is true, while preserving the development bypass. Update
the corresponding monkeypatches in
tests/unit/connectors/test_ibm_cos_connector.py#L20-L31 to mock is_run_mode_saas
and is_run_mode_on_prem rather than IBM_AUTH_ENABLED.

In `@frontend/components/cloud-picker/ingest-settings.tsx`:
- Around line 232-246: Remove the commented-out Table Structure JSX block near
the ingest settings UI, including its wrapper, descriptive text, Switch, and
handlers; leave the surrounding active UI unchanged.

In `@src/models/processors.py`:
- Around line 839-890: Reduce write overhead from _reconcile_shared_owner by
avoiding synchronous per-alias update_by_query calls on every unchanged or
duplicate skip. Prefer batching reconciliation once per sync, scoped to the
connector and user, or gate it on detecting that the shared setting changed
since the previous run; if retaining per-file updates, make them asynchronous
with wait_for_completion=False and ensure process_item is not blocked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9fc0fa60-1e66-418b-b834-db6f15a32048

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb88fd and 5200e57.

📒 Files selected for processing (19)
  • .env.example
  • enhancements/connectors/ibm_cos/connector.py
  • frontend/app/api/queries/useGetSettingsQuery.ts
  • frontend/components/cloud-picker/ingest-settings.tsx
  • frontend/components/connectors/shared-bucket-view.tsx
  • frontend/components/file-browser-dialog.tsx
  • frontend/lib/brand.ts
  • src/api/connectors.py
  • src/api/settings/endpoints.py
  • src/api/settings/models.py
  • src/config/settings.py
  • src/models/processors.py
  • tests/unit/api/test_connector_change_detection.py
  • tests/unit/config/test_show_shared_upload_toggle.py
  • tests/unit/connectors/test_connector_file_type_validation.py
  • tests/unit/connectors/test_ibm_cos_connector.py
  • tests/unit/test_connector_sync_bucket_filter_shared.py
  • tests/unit/test_reconcile_shared_owner.py
  • tests/unit/test_shared_flag.py

Comment on lines +198 to 217
const runSelectedFilesSync = (
files: BucketDuplicateFile[],
replaceDuplicates = false,
) => {
syncMutation.mutate(
{
onSuccess: (result) => {
invalidate();
if (result.task_ids?.length) {
// The container path may return two tasks (new files + changed files);
// track them all.
trackIngestTasks(result.task_ids);
onDone();
} else {
toast.info(
result.message ?? "No files found in the selected buckets.",
);
}
},
onError: (err) => {
trackProcessFailure({
processType: "Ingestion",
process: "Document Upload",
category: "Knowledge",
source: "connector",
connector_type: connector.type,
resultValue: err instanceof Error ? err.message : "Sync failed",
});
toast.error(err instanceof Error ? err.message : "Sync failed");
connectorType: connector.type,
body: {
connection_id: connector.connectionId!,
selected_files: files,
settings: ingestSettings,
replace_duplicates: replaceDuplicates,
shared: showSharedToggle
? (ingestSettings.shared ?? false)
: undefined,
},
},
{ onSuccess: onSyncSuccess, onError: onSyncError },
);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether ConnectorSyncBody/connector_sync ever reads a connection hint,
# and whether multiple active connections per connector_type are supported.
rg -n 'connection_id' src/api/connectors.py
rg -n -A5 'class ConnectorSyncBody' src/api/connectors.py

Repository: langflow-ai/openrag

Length of output: 5067


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the backend sync body and resolution logic around connection_id.
sed -n '600,920p' src/api/connectors.py

# Inspect the two frontend call sites in context.
sed -n '170,235p' frontend/components/connectors/shared-bucket-view.tsx
sed -n '130,210p' frontend/components/file-browser-dialog.tsx

Repository: langflow-ai/openrag

Length of output: 16970


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the sync handler and any pydantic config that could change handling of extra fields.
sed -n '840,940p' src/api/connectors.py
rg -n "extra\s*=" src/api/connectors.py src -g'*.py'
rg -n "class Config|model_config" src/api/connectors.py src -g'*.py'

Repository: langflow-ai/openrag

Length of output: 4904


Preserve the connection used for duplicate checks in sync requests ConnectorSyncBody still has no connection_id, so connector_sync can fall back to the first working connection and run the overwrite/skip ingest against a different bucket than the one that was previewed. shared-bucket-view.tsx is already sending a field the backend ignores, and file-browser-dialog.tsx doesn’t send one at all; thread connection_id through the sync body and reuse it in the sync handler.

📍 Affects 2 files
  • frontend/components/connectors/shared-bucket-view.tsx#L198-L217 (this comment)
  • frontend/components/file-browser-dialog.tsx#L152-L186
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/connectors/shared-bucket-view.tsx` around lines 198 -
217, The sync request currently sends connection_id from shared-bucket-view.tsx,
but ConnectorSyncBody and the backend sync handler do not preserve or use it,
while file-browser-dialog.tsx omits it. Add connection_id to ConnectorSyncBody,
send the selected connection ID from both runSelectedFilesSync flows in
shared-bucket-view.tsx and file-browser-dialog.tsx, and update connector_sync to
reuse that ID for duplicate checks and ingest instead of falling back to the
first working connection.

Comment thread src/api/connectors.py
Comment on lines +78 to +86
def _is_unmapped_keyword_agg_error(err: Exception) -> bool:
"""True when a terms aggregation failed because the target field is an
analyzed `text` field without fielddata enabled — the error OpenSearch
raises for connector_file_id on indices that predate its addition to the
explicit `keyword` mapping in config/settings.py."""
msg = str(err)
return "Text fields are not optimised" in msg or "fielddata" in msg


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Narrow the unmapped-agg check. or "fielddata" in msg is too broad and can retry on unrelated fielddata errors; that can still fall through to the outer empty-result path and hide a real failure. Restrict this helper to the actual "Text fields are not optimised" mapping error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/connectors.py` around lines 78 - 86, Update
_is_unmapped_keyword_agg_error to return true only when the error message
contains the exact “Text fields are not optimised” mapping-error text; remove
the broad fielddata substring check so unrelated fielddata failures are
propagated normally.

Comment thread src/api/connectors.py
Comment on lines +774 to +789
async def _classify_bucket_connector_duplicates(
connector,
connector_type: str,
bucket_filter: list[str],
session_manager,
user_id: str,
jwt_token: str | None,
) -> dict[str, Any]:
"""Preview a bucket_filter sync: classify remote blobs new/changed/unchanged
without ingesting anything, mirroring the reconciliation in connector_sync's
bucket_filter branch. "changed" blobs are reported as duplicates (they would
overwrite an already-indexed version); "unchanged" blobs are silently
dropped (the real sync would skip them too); "new" blobs are returned as
``non_duplicate_files`` so the caller can sync just those when the user
chooses to skip duplicates.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Docstring contradicts the function's actual (and correct) behavior.

The docstring says this classifies blobs "new/changed/unchanged" mirroring connector_sync's modified-time reconciliation, but the implementation (and the inline comment at lines 821-827) explicitly does existence-only classification, deliberately ignoring modified_time. Future maintainers reading just the docstring will misunderstand the semantics.

📝 Suggested fix
-    """Preview a bucket_filter sync: classify remote blobs new/changed/unchanged
-    without ingesting anything, mirroring the reconciliation in connector_sync's
-    bucket_filter branch. "changed" blobs are reported as duplicates (they would
-    overwrite an already-indexed version); "unchanged" blobs are silently
-    dropped (the real sync would skip them too); "new" blobs are returned as
-    ``non_duplicate_files`` so the caller can sync just those when the user
-    chooses to skip duplicates.
-    """
+    """Preview a bucket_filter sync: classify remote blobs as duplicate/non-duplicate
+    purely by connector_file_id existence (same semantics as the OAuth connector
+    duplicate check) — NOT the new/changed/unchanged modified-time gate the real
+    bucket_filter sync in connector_sync uses. "duplicate" blobs already exist
+    under this connector_type; "non_duplicate" blobs are returned so the caller
+    can sync just those when the user chooses to skip duplicates.
+    """
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def _classify_bucket_connector_duplicates(
connector,
connector_type: str,
bucket_filter: list[str],
session_manager,
user_id: str,
jwt_token: str | None,
) -> dict[str, Any]:
"""Preview a bucket_filter sync: classify remote blobs new/changed/unchanged
without ingesting anything, mirroring the reconciliation in connector_sync's
bucket_filter branch. "changed" blobs are reported as duplicates (they would
overwrite an already-indexed version); "unchanged" blobs are silently
dropped (the real sync would skip them too); "new" blobs are returned as
``non_duplicate_files`` so the caller can sync just those when the user
chooses to skip duplicates.
"""
async def _classify_bucket_connector_duplicates(
connector,
connector_type: str,
bucket_filter: list[str],
session_manager,
user_id: str,
jwt_token: str | None,
) -> dict[str, Any]:
"""Preview a bucket_filter sync: classify remote blobs as duplicate/non-duplicate
purely by connector_file_id existence (same semantics as the OAuth connector
duplicate check) — NOT the new/changed/unchanged modified-time gate the real
bucket_filter sync in connector_sync uses. "duplicate" blobs already exist
under this connector_type; "non_duplicate" blobs are returned so the caller
can sync just those when the user chooses to skip duplicates.
"""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/connectors.py` around lines 774 - 789, Update the docstring of
_classify_bucket_connector_duplicates to describe existence-only classification:
treat already-existing remote blobs as duplicates and unseen blobs as
non-duplicates, without comparing modified_time or claiming to mirror
modified-time reconciliation. Keep the documented handling of duplicate and new
blobs consistent with the function’s actual return behavior.

Comment thread src/api/connectors.py
Comment on lines +790 to +802
original_buckets = connector.bucket_names
connector.bucket_names = bucket_filter
try:
all_files: list[dict[str, Any]] = []
page_token = None
while True:
result = await connector.list_files(page_token=page_token)
all_files.extend(result.get("files", []))
page_token = result.get("next_page_token")
if not page_token:
break
finally:
connector.bucket_names = original_buckets

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether get_connector()/connection_manager.get_connector() caches connector instances
rg -n -A8 'async def get_connector' src/connectors/service.py
rg -n -A15 'def get_connector' src -g '*.py' | grep -A15 'connection_manager'

Repository: langflow-ai/openrag

Length of output: 4343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- connection_manager cache ---'
sed -n '420,500p' src/connectors/connection_manager.py

echo
echo '--- connector_service get_connector ---'
sed -n '1,80p' src/connectors/service.py

echo
echo '--- connector list_files and bucket_names usage ---'
rg -n -A8 -B8 'bucket_names|async def list_files|def list_files' src/connectors -g '*.py'

echo
echo '--- connector_sync bucket_filter branch ---'
rg -n -A30 -B10 'bucket_filter' src/api/connectors.py

Repository: langflow-ai/openrag

Length of output: 40353


Avoid mutating shared connector.bucket_names
connection_manager.get_connector() returns a cached authenticated connector per connection_id, so this try/finally can race with another duplicate-check or sync on the same connection and list files with the wrong bucket_filter. Pass the filter into list_files or use a per-request connector copy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/connectors.py` around lines 790 - 802, Update the file-listing flow
around connector.list_files so it does not mutate the shared
connector.bucket_names returned by connection_manager.get_connector(). Pass
bucket_filter through the list_files API for each paginated request, or create a
per-request connector copy, while preserving pagination and filtering behavior.

@mpawlow mpawlow left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review 1

  • See Major PR comment (1a) for assessment / evaluation
  • See Normal PR comments (1b) (1c) (1f) (1g) for consideration
  • See Minor PR comments (1d) (1e) (1h) for optional consideration
  • ⚠️ Note: Most of these PR review comments are carried over from PR #2077
  • ✅ Verified: All relevant changes from PR #2077 are cleanly applied to this PR #2096.
    • Given that this PR's changes exactly represents what will be deployed to SaaS, let me know if it makes sense to address the remaining issues in a subsequent PR 👀

Comment thread src/api/connectors.py
return orphan_ids


class ConnectorSyncBody(BaseModel):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(1a) [Major] connection_id is not threaded through the real sync request, only the duplicate-check preview

Problem

  • ConnectorCheckDuplicatesBody (used by /check-duplicates) has a connection_id field, so the duplicate-check preview runs against a specific connection when the user has more than one active connection of the same connector_type.
  • ConnectorSyncBody (used by the actual connector_sync ingest call) has no connection_id field at all — it always falls back to "the first connection that actually works." shared-bucket-view.tsx sends connection_id in its sync body anyway, but since ConnectorSyncBody doesn't declare that field, Pydantic's default extra="ignore" behavior silently drops it.
  • file-browser-dialog.tsx's submitSync doesn't even attempt to send it.

Background Information

Code References

  • src/api/connectors.py:612-629 (ConnectorSyncBody — no connection_id)
  • src/api/connectors.py:632-639 (ConnectorCheckDuplicatesBody — has connection_id)
  • src/api/connectors.py:1026,1075-1090 (connector_sync connection resolution — first-working fallback only)
  • frontend/components/connectors/shared-bucket-view.tsx:180,206 (sends connection_id that the backend ignores)
  • frontend/components/file-browser-dialog.tsx:152-186 (submitSync — no connection_id sent at all)

Potential Solution

  • Add connection_id: str | None = None to ConnectorSyncBody, and in connector_sync prefer body.connection_id (mirroring the if body.connection_id: ... else: first-working pattern already in connector_check_duplicates) before falling back to "first working connection."

Alternative Solutions

  • Have the duplicate-check response echo back the resolved connection_id, and require the frontend to pass it through unmodified on the follow-up sync call (makes the coupling explicit at the API boundary).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@coderabbitai can you create an issue wrt to the review comments above and assign it to me and @mpawlow

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="langflow-ai/openrag"
body_file="$(mktemp)"
trap 'rm -f "$body_file"' EXIT

cat >"$body_file" <<'EOF'
## Summary

Ensure that the connector used for duplicate preview is also used for the subsequent ingestion request.

Today, `ConnectorCheckDuplicatesBody` accepts `connection_id`, so `/check-duplicates` can inspect a specific active connection. The real `connector_sync` endpoint, however, does not declare or honor `connection_id`; it resolves the first active connection that authenticates. Because Pydantic ignores unknown fields by default, a `connection_id` sent by the bucket UI is silently discarded. The file-browser flow does not send it at all.

With multiple active connections of the same connector type, users can preview duplicates from connection A and then overwrite or ingest files from connection B.

## Required changes

- Add an optional `connection_id` field to `ConnectorSyncBody` in `src/api/connectors.py`.
- Update `connector_sync` connection resolution to prefer and validate `body.connection_id`, matching the behavior of `connector_check_duplicates`; retain the first-working-connection behavior only as the fallback when it is omitted.
- Thread the resolved/selected connection ID through both follow-up sync flows:
  - `frontend/components/connectors/shared-bucket-view.tsx`
  - `frontend/components/file-browser-dialog.tsx`
- Add regression coverage for multiple active connections, including that a supplied connection ID is used for the actual sync and that the duplicate preview and sync target the same connection.

## Rationale

Duplicate handling must act on the source that was previewed. Otherwise, a user can make an overwrite/skip decision based on one bucket connection while the application ingests from another connection of the same type.

## Acceptance criteria

- A sync request with a valid `connection_id` uses that exact active, authenticated connection.
- An invalid, inactive, or unauthenticated requested connection produces an appropriate error rather than silently switching to another connection.
- When no `connection_id` is provided, existing first-working-connection fallback behavior remains intact.
- The bucket shared-ingest and file-browser duplicate workflows send the same connection ID to both duplicate checking and sync.
- Automated tests cover the multi-connection scenario.

## Backlinks

- PR: https://github.com/langflow-ai/openrag/pull/2096
- Review discussion: https://github.com/langflow-ai/openrag/pull/2096#discussion_r3587743320
- Requested by: `@edwinjosechittilappilly`
EOF

gh issue create \
  --repo "$repo" \
  --title "Preserve connector connection_id from duplicate preview through sync" \
  --body-file "$body_file" \
  --assignee edwinjosechittilappilly \
  --assignee mpawlow

Length of output: 2989


@edwinjosechittilappilly, created and assigned the follow-up issue to you and @mpawlow:

#2111Preserve connector connection_id from duplicate preview through sync

Comment thread src/api/connectors.py
``non_duplicate_files`` so the caller can sync just those when the user
chooses to skip duplicates.
"""
original_buckets = connector.bucket_names

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(1b) [Normal] Duplicate-check preview mutates a cached, shared connector's bucket_names without per-request isolation

Problem

  • _classify_bucket_connector_duplicates temporarily overwrites connector.bucket_names = bucket_filter, calls connector.list_files(...), then restores it in a finally block. connector_service.get_connector(connection_id) (via connection_manager) returns a cached connector instance keyed by connection_id (self.active_connectors[connection_id]), not a fresh instance per request. If two requests for the same connection race (e.g. two browser tabs, or a preview check overlapping with a real sync), one request's temporary bucket_names mutation can leak into the other's list_files() call, causing it to list the wrong buckets.

Background Information

  • This mutate/restore-in-finally pattern is not unique to the new code — the same pattern already exists in connector_sync's own bucket_filter branch (src/api/connectors.py:1198-1209) and elsewhere in the file (e.g. :2381-2386), so this PR extends an existing architectural shortcut rather than introducing a new one.

Code References

  • src/api/connectors.py:790-801 (_classify_bucket_connector_duplicates)
  • src/api/connectors.py:1198-1209 (same pattern in connector_sync, pre-existing)
  • src/connectors/connection_manager.py:431-440 (get_connector connector caching by connection_id)

Potential Solution

  • Pass bucket_filter as a parameter into connector.list_files(bucket_filter=..., page_token=...) instead of mutating shared instance state, so no shared mutable state is touched across concurrent requests.

Alternative Solutions

  • Acquire a per-connection_id asyncio lock around the mutate/restore window as a smaller, lower-risk interim fix if changing the list_files signature is out of scope for this backport.

Comment thread src/api/connectors.py
return connector_type in ["google_drive", "sharepoint", "onedrive"]


def _is_unmapped_keyword_agg_error(err: Exception) -> bool:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(1c) [Normal] _is_unmapped_keyword_agg_error's broad substring match can mask unrelated aggregation failures

Problem

  • _is_unmapped_keyword_agg_error returns True on "Text fields are not optimised" in msg or "fielddata" in msg. The bare "fielddata" substring check is broader than the specific legacy-mapping error this helper is meant to detect, so an unrelated OpenSearch error that happens to mention "fielddata" would also trigger the .keyword-field retry path instead of surfacing as a real failure.

Background Information

  • CodeRabbit flagged this as Major; but it is Normal because the retry re-runs the same query with a renamed field — if the actual problem is unrelated, the retry itself will very likely still raise (there's no swallowing except around the retry call), so the most common failure mode is a wasted retry rather than a silently wrong result. The narrower risk is a transient/unrelated fielddata error that happens to succeed on retry, returning results computed against a re-issued query that may not reflect the original triggering condition.

Code References

  • src/api/connectors.py:78-84 (_is_unmapped_keyword_agg_error)
  • src/api/connectors.py:127-137 (first call site / retry)
  • src/api/connectors.py:291-297 (second call site / retry)

Potential Solution

  • Narrow the check to the specific mapping-error text ("Text fields are not optimised" / the associated OpenSearch error type), dropping the standalone "fielddata" substring match.

Alternative Solutions

  • Match on the specific OpenSearch exception type/error.type field (e.g. illegal_argument_exception with a caused_by mapping-related reason) instead of a raw string substring match, which is generally more robust against message-text drift across OpenSearch versions.

Comment thread src/api/connectors.py
}


async def _classify_bucket_connector_duplicates(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(1d) [Minor] Docstring on _classify_bucket_connector_duplicates contradicts its actual (correct) behavior

Problem

  • The docstring claims the function classifies blobs as "new/changed/unchanged" mirroring connector_sync's modified-time reconciliation. The implementation — and its own inline comment a few lines below — explicitly does existence-only classification (any blob whose ID already exists is a "duplicate", regardless of modification time). A future maintainer reading only the docstring will misunderstand the semantics.

Code References

  • src/api/connectors.py:774-789 (docstring)
  • src/api/connectors.py:820-827 (inline comment describing the actual, correct behavior)

Potential Solution

  • Update the docstring to describe existence-only classification, as CodeRabbit's suggested diff does — align it with the inline comment already in the function body.

Comment thread src/api/connectors.py
chooses to skip duplicates.
"""
original_buckets = connector.bucket_names
connector.bucket_names = bucket_filter

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(1f) [Normal] Existence-based duplicate check triggers the confirm dialog (and a full re-embed) on every routine bucket re-sync, even with zero remote changes

Problem

  • Because _classify_bucket_connector_duplicates flags any previously-ingested blob as a duplicate (see 1d), re-clicking "Ingest" on a bucket that was already fully synced — with no changes at the source — always surfaces the overwrite dialog. If the user picks "Overwrite duplicates" (the more prominent, styled button), every unchanged file gets re-ingested/re-embedded unnecessarily.

Background Information

Code References

  • src/api/connectors.py:790-827 (existence-based classification)
  • src/api/connectors.py:1198-1238 (existing, correct modified-time-based reconciliation it should mirror)

Potential Solution

  • Have _classify_bucket_connector_duplicates reuse the existing modified_time-based reconciliation helper already used in connector_sync's bucket_filter branch, and only surface true "changed" blobs as duplicate_files; drop "unchanged" blobs from both lists so the dialog only appears when there's a real conflict.


const checkData =
(await checkResponse.json()) as BucketDuplicateCheckResponse;
const duplicateNames = checkData.duplicate_names || [];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(1g) [Normal] Redundant full bucket listing on every "no duplicates" ingest

Problem

  • ingestSelected in SharedBucketView calls check-duplicates, which lists every object across the selected buckets (_classify_bucket_connector_duplicates, unbounded pagination). When duplicateCount === 0, the frontend discards that listing and calls runBucketSync(), which re-triggers connector_sync's bucket_filter branch — listing the same buckets from scratch again. For large buckets this doubles listing latency/cost on every ingest click, when the check step already has the complete non_duplicate_files list in hand.

Background Information

Potential Solution

  • When duplicateCount === 0, call runSelectedFilesSync(checkData.non_duplicate_files, false) directly instead of runBucketSync(), avoiding the second full listing.

duplicateFiles: BucketDuplicateFile[];
nonDuplicateFiles: BucketDuplicateFile[];
} | null>(null);
const isOverwriteConfirmedRef = useRef(false);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(1h) [Minor] Fragile ref-based workaround for dialog close/overwrite race, with no test coverage

Problem

  • isOverwriteConfirmedRef exists to suppress handleDuplicateDialogOpenChange's "skip" branch from re-firing when DuplicateHandlingDialog's overwrite handler calls onOpenChange(false) right after onOverwrite(). It works, but depends on pendingDuplicates still being non-null in a stale closure at the moment onOpenChange(false) fires synchronously after onOverwrite(), before React flushes the state update clearing it. Any future change to DuplicateHandlingDialog's call order could silently break this, and no frontend/e2e test exercises either the overwrite or skip path.

Background Information

Potential Solution

  • Have a single source of truth for "why did the dialog close" — e.g., have the overwrite handler own closing the dialog and clearing pendingDuplicates in one state update, and have the dialog's own Cancel/backdrop-close path call a distinct onSkip callback rather than overloading onOpenChange. Add a Playwright test covering: no-duplicates auto-sync, duplicates→overwrite, duplicates→skip, duplicates→dismiss-without-choice.

…te-handling-bucket-connectors

# Conflicts:
#	frontend/components/connectors/shared-bucket-view.tsx
#	frontend/components/file-browser-dialog.tsx
#	src/api/connectors.py
@github-actions github-actions Bot added bug 🔴 Something isn't working. and removed bug 🔴 Something isn't working. labels Jul 16, 2026
@github-actions github-actions Bot added bug 🔴 Something isn't working. and removed bug 🔴 Something isn't working. labels Jul 16, 2026
@edwinjosechittilappilly
edwinjosechittilappilly merged commit fe9425d into main Jul 16, 2026
39 checks passed
@github-actions
github-actions Bot deleted the backport/2077-duplicate-handling-bucket-connectors branch July 16, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend 🔷 Issues related to backend services (OpenSearch, Langflow, APIs) bug 🔴 Something isn't working. frontend 🟨 Issues related to the UI/UX tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants