Word Document Extraction (WordExtractor)#
Overview#
WordExtractor in api/core/rag/extractor/word_extractor.py parses .docx files for Dify's RAG ingestion pipeline, producing a single Document with Markdown-formatted text 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 substituted 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 is delete=False + manual os.unlink() in close(), but this 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 toext_storageunderimage_files/<tenant_id>/<uuid>.<ext>, creates anUploadFilerecord, and maps the part object β. - External image URLs (
rel.is_external == True): downloads viaremote_fetcher, saves the same way, and mapsr_idβ the same preview URL .
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:anchorinternal bookmarks are intentionally left as plain text β only externally-referenced links with anr:idrelationship are converted to Markdown .
Table Parsing & Pipe Escaping#
Tables are rendered as GitHub-Flavored Markdown by _table_to_markdown(), which calls _parse_row() β _parse_cell() β _parse_cell_paragraph(). Merged cells (column-span via cell.grid_span) are handled by filling spanned columns with empty strings .
Pipe character escaping: Cell content containing literal | characters (e.g., a | b) must be escaped to \| to preserve Markdown table structure. Without escaping, the pipe becomes an extra column separator, corrupting the table row. PR #41991 added this escaping in _parse_row .
Key Files#
| 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 used for remote file and image downloads |