Skip to content

fix(connectors): index Office documents and PDFs from SharePoint and OneDrive - #6785

Merged
waleedlatif1 merged 4 commits into
stagingfrom
fix/connector-office-document-parsing
Aug 17, 2026
Merged

fix(connectors): index Office documents and PDFs from SharePoint and OneDrive#6785
waleedlatif1 merged 4 commits into
stagingfrom
fix/connector-office-document-parsing

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Problem

A customer pointed a SharePoint connector at a document library of SOPs and got 0 documents on every sync — no error, no failed rows, status: completed. Three days were spent re-checking the site URL, the folder path, and the credential, all of which were correct.

The SharePoint and OneDrive connectors filtered their listings against a 12-item plain-text extension whitelist:

.txt .md .html .htm .csv .json .xml .yaml .yml .log .rst .tsv

Anything else — .docx, .pdf, .xlsx, .pptx — failed isSupportedTextFile during listing and was dropped before it could become a document. The folder resolved correctly; it just had nothing the filter would accept, so the sync legitimately reported success with zero documents. That outcome is indistinguishable from a wrong folder path, which is what made it so hard to diagnose.

Two things worth noting:

  • This is not a regression. Both whitelists are byte-identical to the versions that shipped with the connectors (SharePoint in feat(mothership): mothership #3411, 2026-03-13) and have never been edited.
  • Sim already parses every one of these formats. lib/file-parsers covers pdf doc docx xls xlsx ppt pptx, and the same customer had a .docx sitting in the same knowledge base, uploaded manually and processed successfully. The parsers existed; the connectors just never called them — downloadFileContent did a raw buffer.toString('utf8').

Confirming evidence from the affected workspace: the customer's working connector on the same site and credential holds 64 documents, and all 64 are .csv, .htm, or .txt. Zero Office documents across the entire library tree — exactly the shape the whitelist imposes.

Change

Adds three helpers to apps/sim/connectors/utils.ts, next to the existing CONNECTOR_MAX_FILE_BYTES that already aligns the connector size cap with the upload cap:

  • CONNECTOR_INDEXABLE_EXTENSIONS / isIndexableConnectorFile(name) — the union of the text formats connectors already accepted and the binary document formats the knowledge base can parse.
  • extractConnectorText(buffer, fileName) — UTF-8 for text formats, htmlToPlainText for HTML, and parseBuffer for binary document formats.

sharepoint.ts and onedrive.ts now use those instead of their local whitelists, and their downloadFileContent returns a Buffer rather than a pre-decoded string.

The change is deliberately additive. CONNECTOR_TEXT_EXTENSIONS and CONNECTOR_PARSED_EXTENSIONS are kept as separate lists precisely so a format that synced yesterday takes the identical path today. Routing .csv through CsvParser or .json through the JSON parser would reformat content that is already embedded, changing every existing connector document on its next re-index — so those keep bypassing the parsers. Only the newly-accepted binary formats reach parseBuffer, which is also where the OOXML zip-bomb guard and each parser's own extraction limits (MAX_PDF_TEXT_CHARS, the 60s PDF deadline) apply.

Also adds a per-page logger.info counting files skipped for an unsupported extension, with the distinct extensions seen. Unsupported files are counted, not turned into failed document rows — a library full of images would otherwise flood the knowledge base UI. The goal is only to make "0 documents" explicable in the run trace.

Tests

apps/sim/connectors/utils.test.ts:

  • Office/PDF formats accepted; previously-supported text formats still accepted; .png/.mp4/.zip still rejected; extensionless and trailing-dot names rejected; case-insensitive
  • .docx routes through parseBuffer with the right extension
  • regression guard: .txt/.csv/.yaml/.tsv/.xml decode as UTF-8 and never invoke a parser
  • HTML reduces to plain text without a parser; unknown extension falls back to UTF-8; a parser failure propagates so the sync records a failed document

apps/sim/connectors/sharepoint/sharepoint.test.ts:

  • an end-to-end listing containing .docx/.pdf/.xlsx/.pptx/.txt returns all five (this is the reported bug)
  • a listing of .png/.mp4/.txt still returns only the .txt

Verified the new tests can fail: narrowing CONNECTOR_INDEXABLE_EXTENSIONS back to the text-only set turns exactly 3 of them red.

  • vitest run connectors/ — 695 passed (25 files)
  • tsgo --noEmit -p apps/sim/tsconfig.json — clean apart from a pre-existing unrelated Cannot find module 'mssql'
  • bun run check:audits — 29/29 pass

Follow-ups not in this PR

  • PDF text bombs. Enabling .pdf in connectors puts the sync path behind the same PDF extraction concern tracked separately for the upload path. The parser's own char/time caps apply and the size cap already matches manual upload, so this does not widen the limit — but it does add a second entry point worth resolving there.
  • Other connectors. box, dropbox, s3, sftp, and google-drive have their own file-type handling that was not touched here. If they carry the same gap, they can adopt extractConnectorText in a follow-up.

…OneDrive

The SharePoint and OneDrive connectors filtered their listings against a
12-item plain-text extension whitelist, so a document library of .docx, .pdf
or .xlsx files synced as "success, 0 documents" — no document, no failed row,
and no log line, which is indistinguishable from a wrong folder path. Both
whitelists had been unchanged since the connectors shipped, and Sim already
parses all of these formats for a manually uploaded knowledge base document.

Adds a shared `extractConnectorText` in connectors/utils that routes binary
document formats through the same `parseBuffer` the upload path uses, so the
OOXML zip-bomb guard and each parser's extraction limits apply. The
previously-accepted text formats stay on their exact existing path: sending
.csv through CsvParser would silently reformat every already-indexed connector
document on its next re-index.

Also logs a per-page count of files skipped for an unsupported extension.
Unsupported files are counted rather than turned into failed document rows, so
a library full of images does not fill the knowledge base with noise.
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 17, 2026 10:13pm

Request Review

@cursor

cursor Bot commented Aug 17, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes connector sync and knowledge-base indexing for Microsoft Graph drives and centralizes file parsing; behavior is well covered by tests but affects production document content and re-index paths.

Overview
SharePoint and OneDrive no longer drop Office and PDF files during listing because of a plain-text-only extension filter. They use shared isIndexableConnectorFile and extractConnectorText so downloads stay as bytes and binary formats go through the same parseBuffer path as manual uploads, while existing text formats still decode as UTF-8 without re-parsing.

Failed or placeholder extractions (legacy OLE, image-only decks, empty PDFs) surface as skipped documents via ConnectorTextExtractionError and actionable skip reasons instead of silent drops or junk in the index. Listing logs now count unsupported extensions per folder so “success, 0 documents” is easier to distinguish from a bad path.

lib/file-parsers replaces lazy require() registration with a static Map registry (avoids a silent empty registry), adds OpenDocumentParser and more extension aliases, and marks degraded doc/ppt fallback output with metadata.degraded for automated callers.

Reviewed by Cursor Bugbot for commit 1bb1b2b. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR enables SharePoint and OneDrive connectors to index Office, PDF, and OpenDocument files while preserving the existing decoding path for text formats.

  • Centralizes connector extension detection and binary-document text extraction.
  • Routes supported binary formats through the shared parser registry and decompression safeguards.
  • Marks degraded legacy parser fallbacks as failed connector documents instead of indexing diagnostic or scraped content.
  • Adds parser-registry, format-routing, and connector regression coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported legacy parser fallback issue is addressed by rejecting degraded extraction results and preserving them as visible failed documents.

Important Files Changed

Filename Overview
apps/sim/connectors/utils.ts Adds centralized indexable-extension detection, binary parsing, and rejection of empty or degraded extraction results.
apps/sim/connectors/sharepoint/sharepoint.ts Expands SharePoint listing and hydration to supported document formats while preserving size and extraction-failure handling.
apps/sim/connectors/onedrive/onedrive.ts Applies the shared document extraction and unsupported-extension diagnostics to OneDrive.
apps/sim/lib/file-parsers/index.ts Replaces the failure-tolerant dynamic registry with explicit parser mappings for the newly supported format variants.
apps/sim/lib/file-parsers/doc-parser.ts Declares legacy DOC fallback output as degraded so automated connector syncs do not index it.
apps/sim/lib/file-parsers/pptx-parser.ts Declares PowerPoint fallback output as degraded so connector syncs surface extraction failure instead of fabricated content.
apps/sim/lib/file-parsers/opendocument-parser.ts Adds guarded ODT and ODP parsing without a raw-byte fallback.
apps/sim/lib/file-parsers/types.ts Extends parser metadata and supported-format contracts for degraded extraction and additional Office/OpenDocument variants.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  List[SharePoint or OneDrive listing] --> Filter{Indexable extension?}
  Filter -- No --> Log[Count unsupported extension]
  Filter -- Yes --> Stub[Create deferred document stub]
  Stub --> Download[Download with byte limit]
  Download --> Kind{Text or binary?}
  Kind -- Text --> Decode[UTF-8 or HTML-to-text]
  Kind -- Binary --> Parse[Shared file parser]
  Parse --> Quality{Content usable and not degraded?}
  Quality -- Yes --> Index[Index extracted text]
  Quality -- No --> Skip[Persist visible failed/skipped document]
  Decode --> Index
Loading

Reviews (3): Last reviewed commit: "fix(file-parsers): resolve the parser re..." | Re-trigger Greptile

Comment thread apps/sim/connectors/utils.ts Outdated
`DocParser` and `PptxParser` never throw by design — on a legacy OLE `.doc`/`.ppt`
or a deck with no extractable text they return a placeholder sentence or scraped
ZIP internals so an interactive upload still shows the user something. Verified
against real OOXML fixtures: an image-only `.pptx` yields 1.9KB of
`[Content_Types].xml…` as "content", and a legacy `.ppt` yields "Unable to
extract text from PowerPoint file."

A connector sync would embed that into the vector index at scale, so it needs to
tell a real extraction from a fabricated one. Adds a declared `degraded` flag to
`FileParseMetadata`, set by exactly those two fallback paths, rather than having
callers sniff `extractionMethod`. `DocParser`'s plaintext branch stays unflagged:
a text file misnamed `.doc` is a genuine extraction.

`extractConnectorText` now raises `ConnectorTextExtractionError` when a parsed
format comes back degraded or blank, and SharePoint/OneDrive surface it as a
skipped document via the existing `markSkipped` path — so the file appears in the
knowledge base as a failed row telling the user to re-save it as DOCX/PPTX/XLSX,
instead of being silently dropped or indexed as junk.

The upload path is unaffected; it ignores the new flag.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Follow-up: verified the parsers against real fixtures, found a second defect

Question raised: do .pptx files need an additional parser? No — but characterising them turned up something that had to be fixed before this is safe to ship.

I built genuine OOXML archives with jszip and ran the real parser classes (not mocks). Results:

case result
.pptx with slide text method=officeparser, clean text — no new parser needed
.pptx image-only / no text method=fallback1902 chars of [Content_Types].xml… ZIP internals as "content"
.ppt legacy OLE binary method=fallback"Unable to extract text from PowerPoint file…" as "content"
.doc legacy OLE binary method=fallback → placeholder sentence as "content"
.docx with text ✅ mammoth; an empty docx throws
.xlsx, .pdf ✅ clean; garbage input throws

DocParser and PptxParser never throw — that is deliberate, so an interactive upload always shows the user something. But a connector sync would embed those placeholders and ZIP internals into the vector index at scale, and a placeholder sentence is indistinguishable from real content to the retriever.

Fix

Rather than have callers sniff extractionMethod === 'fallback', the parsers now declare it: a degraded?: boolean on FileParseMetadata, set by exactly those two fallback paths. DocParser's plaintext-fallback branch is intentionally not flagged — a text file misnamed .doc is a genuine extraction.

extractConnectorText raises ConnectorTextExtractionError when a parsed format returns degraded or blank content, and SharePoint/OneDrive map it to markSkipped(...) through the existing path oversized files already use. The file then shows up in the knowledge base as a failed row with an actionable reason ("Re-save it as PPTX to index it") instead of being silently dropped or indexed as junk.

The upload path is unaffected — it ignores the new flag.

Also verified while I was in here

  • Runtime boundary. parseBuffer was not previously proven inside the Trigger.dev worker, which is where connector syncs run — the exact hazard CLAUDE.md warns about. It is proven: background/knowledge-processing.ts is a Trigger.dev task that reaches parseBuffer via processDocumentAsync, and its machine comment already reads large-1x // needed for large PDF processing.
  • Machine sizing. knowledge-connector-sync runs on large-2x (4 vCPU / 8 GB, maxDuration 1800s) — strictly better provisioned than the large-1x task that already parses large PDFs. No new OOM risk.
  • Parser-registry fragility (not fixed here, worth knowing). file-parsers/index.ts loads every parser with a bare require() inside a per-parser try/catch that only logs. Under ESM the whole registry silently comes back empty and parseBuffer reports Unsupported file type: docx. Supported types are: with an empty list. It works in the Next/Trigger runtimes, but a bundler change would degrade to "unsupported type" for every format rather than failing loudly.
  • xlsx is very permissiveparseBuffer(Buffer.from('not a spreadsheet'), 'xlsx') "succeeds" and echoes the input as a one-cell sheet. Harmless (the content is the file's text) but noted.

Tests

New apps/sim/lib/file-parsers/degraded-extraction.test.ts pins the degraded contract to real parser behaviour using genuine OOXML fixtures, so if a parser stops setting the flag this breaks. Plus connector-level tests that a degraded file becomes a skipped document with the right reason, and that a text file still bypasses the parsers entirely.

Both new guards were confirmed able to fail: removing degraded: true from PptxParser reddens 2 fixture tests; removing the degraded check in extractConnectorText reddens the unit test and the connector-level skip test.

  • vitest run connectors/ lib/file-parsers/ lib/knowledge/1243 passed (76 files)
  • tsgo --noEmit — clean apart from the pre-existing unrelated Cannot find module 'mssql'
  • bun run check:audits — 29/29

… handle

A document library holds whole format families, not just the headline extension
of each. These all extract correctly with the libraries already installed — they
were simply never registered, so every one of them was reported as an
unsupported file type:

  docm dotx  (WordprocessingML — mammoth reads word/document.xml regardless of
              the package content type)
  xlsm xlsb xltx ods  (SheetJS reads every workbook container natively)
  pptm potx  (PresentationML)
  odt odp    (OpenDocument, via a new OpenDocumentParser)

Verified against real fixtures built with jszip and SheetJS rather than assumed:
officeparser identifies a Buffer by sniffing content with `file-type`, not by the
name we pass, so the routing had to be measured. `ods` goes to the spreadsheet
parser rather than OpenDocumentParser so its output keeps per-sheet structure.

`rtf` is deliberately excluded: nothing bundled extracts it, and DocParser's
plaintext branch would pass its control words through as if they were prose.

Converts the registry from `require()` inside per-parser `try/catch` blocks that
only logged to static imports. Every parser dependency is a regular, non-optional
one, so a resolution failure should fail loudly — the old form produced a silently
**empty** registry in which every format became `Unsupported file type`, with an
empty "Supported types are:" list as the only clue. The heavy extraction libraries
are still deferred inside the individual parsers, and connectors now import the
registry lazily so the ~60 connectors that never touch a file do not pull SheetJS.

Adds registry.test.ts, which exercises the real module: index.test.ts mocks
`@/lib/file-parsers` itself, so it validated its own fake routing table and the
real registry had no coverage at all. The new test gates every member of
SupportedFileType on having a registered parser that supports buffer parsing.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Format audit: what was missing, and what is genuinely impossible

Registered in this push — all already parseable, just never wired up

Verified with real fixtures built via jszip/SheetJS, not assumed. This mattered: officeparser identifies a Buffer by sniffing content with file-type, ignoring the name we pass it, so routing had to be measured rather than reasoned about.

added extracts via why it was missing
docm, dotx mammoth same WordprocessingML package as docx; mammoth reads word/document.xml without consulting the content type
xlsm, xlsb, xltx, ods SheetJS SheetJS reads every workbook container natively, including the binary and OpenDocument ones
pptm, potx officeparser same PresentationML package as pptx
odt, odp officeparser supported by the library all along, no parser was registered

Zero new dependencies. .xlsm in particular is everywhere in enterprise SharePoint.

Why the degraded ones are degraded, and what can actually be done

.doc and .ppt (pre-2007) — these are OLE2 compound binaries, a completely different container from the post-2007 ZIP packages. mammoth and officeparser only read OOXML, so there is nothing to fall back on. This is fixable, but needs a dependency: word-extractor handles legacy .doc well. Legacy .ppt has no maintained JS library — realistically it needs LibreOffice/unoconv, i.e. a conversion service, not a parser. Note .xls is not in this bucket: SheetJS reads BIFF natively, which is why it already works. Meanwhile the skip reason now tells the user to re-save as DOCX/PPTX, which genuinely fixes it in seconds.

Image-only .pptx and scanned PDFs — there is legitimately no text in the file. The bytes are pixels. The only answer is OCR (Tesseract, or a cloud vision API), which is a different class of feature: slow, costly, and a product decision. Nothing a parser can do.

.rtf — deliberately left unsupported, and this is worth calling out because it was a latent trap: DocParser's plaintext-fallback branch accepts RTF (it is >90% printable ASCII) and returns {\\rtf1\\ansi\\deff0… as if it were prose, with no degraded flag. Had I added rtf to the accepted set, we would have silently indexed control words. Fixing it properly needs a dependency (rtf-parser); hand-rolling RTF tokenization is exactly the kind of thing I am not going to do here. It is now excluded and covered by a test.

.msg / .eml — common in document libraries. Both are solvable with dependencies (@kenjiuno/msgreader, mailparser). Worth doing if customers ask; not in scope here.

.one (OneNote), .pages/.key/.numbers, .vsdx — legitimately nothing. OneNote and the Apple iWork formats store text as proprietary binary (iWork uses Snappy-compressed protobuf in an IWA stream) with no maintained JS reader. Visio is an OOXML zip so it is theoretically reachable, but officeparser does not support it and the text is scattered across page XML — a custom extractor, low value.

Happy to add word-extractor (legacy .doc) and/or the email parsers in a follow-up if you want those — say the word and I will open a separate PR. I did not add dependencies unilaterally to a public repo.

Two defects found while auditing

1. The registry could silently support nothing. It loaded every parser with require() inside per-parser try/catch blocks that only logged. Under any loader where those calls fail, the registry came back empty and every format reported Unsupported file type: docx. Supported types are: — note the empty list, which is the tell. Every parser dependency is a regular, non-optional one, so a resolution failure should be loud. Now static imports; the heavy extraction libraries stay deferred inside the individual parsers, and connectors import the registry lazily so the ~60 connectors that never touch a file do not pull SheetJS into their graph.

2. index.test.ts mocks the module it tests. Lines 67–92 replace @/lib/file-parsers with a stub parseFile/isSupportedFileType backed by a hardcoded table, so it validates its own fake routing and the real registry had no coverage at all. That is precisely how defect 1 stayed invisible. Rather than rewrite that file, I added registry.test.ts against the real module: it gates every member of SupportedFileType on having a registered parser that supports buffer parsing, so adding a type to the union without wiring it up now fails a test. parser-formats.test.ts then proves each extension reaches a parser that actually handles that container, end-to-end through parseBuffer — a registry entry pointing at the wrong parser would pass the first test and fail the second.

Verification

  • vitest run lib/ connectors/ app/api/files/ app/api/tools/file/14,805 passed, 29 skipped (1,024 files). Deliberately wide because the static-import conversion touches every parser consumer.
  • vitest run lib/file-parsers/ connectors/ — 829 passed
  • tsgo --noEmit — clean apart from the pre-existing unrelated Cannot find module 'mssql'
  • bun run check:audits — 29/29

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0cbab90. Configure here.

…ect keys

The registry rewrite switched extension lookup from
`Object.keys(parsers).includes(ext)` to a bracket read on an object literal,
which also resolves inherited keys. `PARSERS['constructor']` therefore returned
`Object` — truthy, with no parse methods — so a caller-supplied extension of
`constructor` fell through to "does not support buffer parsing" instead of being
rejected as an unsupported type, and `parseFile` would have raised a TypeError.
It also disagreed with `isSupportedFileType`, which used `Object.hasOwn` and
correctly returned false for the same input.

A Map has no prototype chain to walk, so lookup and support check now agree by
construction. `isSupportedFileType` also guards a non-string argument, which the
try/catch it replaced used to absorb.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Self-review found one defect I introduced — fixed in 1bb1b2b5

Pressure-testing the "strictly better" claim rather than asserting it turned up a regression in my own registry rewrite.

Converting the registry to static imports also changed extension lookup from Object.keys(parsers).includes(ext) to a bracket read on an object literal — which resolves inherited keys. Measured, not theorised:

constructor      isSupportedFileType=false  parseBuffer="Parser for constructor does not support buffer parsing"
toString         isSupportedFileType=false  parseBuffer="Unsupported file type: tostring…"

PARSERS['constructor'] returned Object — truthy, with no parse methods — so a caller-supplied extension of constructor took the wrong branch, and parseFile would have raised a TypeError: parser.parseFile is not a function instead of a clean unsupported-type error. It also disagreed with isSupportedFileType, which used Object.hasOwn and correctly said false for the same input. The old Object.keys().includes() form was immune, so this was mine, not pre-existing.

Low severity — it needs a file literally named *.constructor — but it is exactly the kind of inconsistency that should not ship. Now a Map, which has no prototype chain, so lookup and support check agree by construction. isSupportedFileType also guards a non-string argument, which the try/catch it replaced used to absorb. Covered by it.each over constructor/toString/valueOf/hasOwnProperty/__proto__.

Also verified, since it was the open question on the static-import change

I flagged earlier that parseBuffer had to work in the Trigger.dev worker. Confirmed against production rather than inferred: PDFs uploaded today on v20260817.1 processed to completed with real chunk counts, so every parser module already resolves in that runtime and the require() registry was not silently empty in prod. This change therefore removes a latent hazard and makes it testable — it is not papering over a live outage, and it does not alter which modules the bundler includes (a require() of a static string literal is statically analysable too).

Blast radius for the connector behaviour change is small: 5 active SharePoint connectors in prod, zero OneDrive.

  • vitest run lib/ connectors/ app/api/files/ app/api/tools/file/ — 14,811 passed, 29 skipped
  • tsgo --noEmit — clean apart from the pre-existing unrelated mssql
  • bun run check:audits — 29/29

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1bb1b2b. Configure here.

@waleedlatif1
waleedlatif1 merged commit ef225f9 into staging Aug 17, 2026
30 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/connector-office-document-parsing branch August 17, 2026 22:23
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