Word Document Extraction (WordExtractor)#
Overview#
WordExtractor (api/core/rag/extractor/word_extractor.py) parses .docx files for Dify's RAG ingestion pipeline, producing a single Document with Markdown-formatted content that preserves images, hyperlinks, and tables. It is dispatched by ExtractProcessor for .docx files in both dify and unstructured ETL modes.
Key entry points:
| Method | Purpose |
|---|---|
__init__ | Accepts a local file path or remote URL; downloads URLs via temp file |
extract() | Calls parse_docx() and wraps output in a Document |
parse_docx() | Orchestrates parsing: images β paragraphs β tables |
close() | Idempotent cleanup of the temp file; also called from __del__ |
Remote File Handling & SSRF Protection#
When file_path is a URL, __init__ downloads the file using remote_fetcher.make_request("GET", ...) and writes the response to a tempfile.NamedTemporaryFile . The temp file's path is then used as self.file_path for the rest of the extraction lifecycle.
Remote downloads previously used httpx.get directly; PR #31678 replaced this with remote_fetcher, which routes through ssrf_proxy to prevent SSRF attacks .
β οΈ Windows PermissionError (Known Issue)#
On Windows, NamedTemporaryFile cannot be reopened while its file handle is still open. When python-docx opens self.file_path (the temp file), Windows raises PermissionError: [Errno 13] Permission denied. The workaround β delete=False + manual os.unlink() in close() β is not yet implemented; a # TODO comment marks the spot . Tracked as GitHub Issue #39889.
Image Extraction#
_extract_images_from_docx() iterates doc.part.rels and handles two cases:
- Embedded images (
rel.is_external == False): readsrel.target_part.blob, saves to external storage underimage_files/<tenant_id>/<uuid>.<ext>, creates anUploadFilerecord, and maps the part object β. - External image URLs (
rel.is_external == True): downloads viaremote_fetcher, saves identically, and mapsr_idβ the same preview URL format .
The preview URL base is dify_config.FILES_URL . PR #35975 fixed a regression where INTERNAL_FILES_URL was used instead, making images unreachable from browsers .
UploadFile records are batched and committed after all images are processed; if _session is injected by the caller, the caller controls the commit .
Hyperlink Extraction#
Two hyperlink formats are handled inside parse_paragraph():
-
Modern
w:hyperlinkXML elements βprocess_hyperlink()readsr:id, resolves the relationship ondoc.part.rels, and emits[text](url)for external relationships. Fixed in PR #30360 . -
Legacy
HYPERLINKfield codes (w:fldChar/w:instrText) β a state machine tracksbegin/separate/endtransitions , extracts the URL via regex frominstrText, and captures the visible text from runs betweenseparateandend.
Both formats are also handled inside table cells via _parse_cell_paragraph(). PR #33224 fixed cell-level parsing to support http and mailto links that were previously missed .
w:anchor internal bookmarks are intentionally left as plain text β only externally-referenced links with an r:id relationship are converted to Markdown .
Table Parsing & Pipe Escaping#
Tables are rendered as GFM by _table_to_markdown() via the chain _parse_row() β _parse_cell() β _parse_cell_paragraph(). Merged cells (column-span via cell.grid_span) are handled by filling spanned columns with empty strings .
Cell content containing literal | characters must be escaped to \|; without escaping, the pipe becomes an extra column separator, corrupting the Markdown table row. PR #41991 added this escaping . PR #42002 included a related fix for Markdown table corruption across multiple extractors .
Text Box Extraction (Silent-Drop Fix)#
parse_docx() iterates doc.iter_inner_content() and processes only Paragraph and Table blocks . Text inside Word text boxes (w:txbxContent) lives in a separate drawing subtree and was never reached, silently dropping callouts, sidebars, and pull quotes.
PR #42334 introduced parse_text_boxes(), which finds w:txbxContent descendants in each paragraph's XML and routes them through the existing parse_paragraph() path . Word stores text boxes twice inside mc:AlternateContent (once in mc:Choice, once in mc:Fallback); the fix skips mc:Fallback copies by checking ancestor tags to prevent duplicate extraction.
Key Files & References#
| File | Description |
|---|---|
api/core/rag/extractor/word_extractor.py | Main implementation |
api/tests/unit_tests/core/rag/extractor/test_word_extractor.py | Unit tests: merged cells, hyperlinks, images, remote download, pipe escaping |
api/core/file/remote_fetcher.py | SSRF-safe HTTP client for remote file/image downloads |
Related PRs:
- PR #30360 β Correct DOCX hyperlink extraction
- PR #31678 β SSRF fix for WordExtractor URL download
- PR #33224 β Fix
mailto/httplinks in table cells - PR #35975 β Fix image rendering in knowledge base
- PR #41991 β Escape pipe characters in table cells
- PR #42002 β Resolve Markdown table corruption
- PR #42334 β Read text inside Word text boxes