Skip to content

feat: report why a document failed to load, so a password is told from a damaged file - #414

Merged
PeterStaar-IBM merged 1 commit into
docling-project:mainfrom
wittjeff:fix/load-failure-reason
Oct 27, 2026
Merged

PeterStaar-IBM merged 1 commit into
docling-project:mainfrom
wittjeff:fix/load-failure-reason

Conversation

@wittjeff

Copy link
Copy Markdown
Contributor

Parser side of docling#4829 (complements docling#4833).

Problem

qpdf says why it refuses a document: invalid password with error code qpdf_e_password, unable to find /Root dictionary with qpdf_e_damaged_pdf, and so on. pdf_decoder<DOCUMENT> logged that text and returned false, the pybind loaders returned false, and Python raised the same RuntimeError("Failed to load document with key ...") for every failure. A caller could not tell "needs a password" from "broken file"; docling's default backend wraps that message into DocumentLoadError unchanged, so neither could its users (docling#4829). docling#4833 works around it in docling by scanning the trailer for /Encrypt.

Fix

  • The decoder records qpdf's error code and message when a load fails (load_error.h maps the code to a category: password, damaged, unsupported, io, unknown). File-not-found and unreadable files are recorded as io.
  • Both native parsers keep the record per key until Python collects it through a new take_load_error(key) method; a successful load for the key clears it.
  • DoclingPdfParser.load and DoclingThreadedPdfParser.load raise PdfLoadError (with category and the qpdf detail), or PdfPasswordError for an encrypted document, which also tells whether a password was supplied: qpdf reports a missing and a wrong password with the same error, the Python layer knows which it was.

Both exceptions are subclasses of RuntimeError and the message keeps the former Failed to load document with key ... prefix, so existing handlers (docling's except (RuntimeError, ValueError) included) keep working while gaining the reason:

Failed to load document with key key=...: the document is encrypted and requires a password
Failed to load document with key key=...: the supplied password is incorrect
Failed to load document with key key=...: ... unable to find /Root dictionary

docling can later replace the trailer probe of docling#4833 with isinstance(e, PdfPasswordError) once this is released.

Tests

tests/test_unit_load_failure_reason.py, for both parsers: an encrypted document (written with pypdf, as test_unit_page_count.py already does) without a password raises PdfPasswordError with password_supplied=False, with a wrong password password_supplied=True, with the right one it loads; garbage raises PdfLoadError with category damaged, is not a password error, and keeps the RuntimeError type and message prefix; a missing file is category io; a failed load does not taint a following successful load of the same bytes. tests/test_unit_load_failure.py from #301 passes unchanged.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @wittjeff, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@PeterStaar-IBM

Copy link
Copy Markdown
Member

@wittjeff Can you fix the merge conflict?

… a damaged file

qpdf says why it refuses a document (an invalid password, a file it
could not repair, ...), but the loaders discarded that and every failure
surfaced as the same RuntimeError("Failed to load document with key ...").
docling's default backend wraps that text unchanged, so an encrypted PDF
was indistinguishable from a corrupt one (docling#4829).

The decoder now records qpdf's error code and message when a load fails;
both native parsers keep the record per key until Python collects it
through take_load_error(); load() raises PdfLoadError with a category
(password, damaged, unsupported, io, unknown) and the qpdf detail, or
PdfPasswordError for an encrypted document, which also tells whether a
password was supplied. Both are RuntimeError subclasses and keep the old
message prefix, so existing handlers keep working.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeff Witt <1848307+wittjeff@users.noreply.github.com>
@wittjeff
wittjeff force-pushed the fix/load-failure-reason branch from 9afcb37 to 9ff5ff0 Compare October 25, 2026 19:25
@wittjeff

Copy link
Copy Markdown
Contributor Author

@PeterStaar-IBM Done — rebased onto current main and force-pushed.

The only conflict was in src/parse/pdf_decoders/document.h, where #415 added ensure_page_content_present() / qpdf_reported_damage at the same spots this PR adds record_load_error() and the load_error_* fields. Both sides are kept; the PR's own diff is otherwise unchanged. tests/test_unit_load_failure_reason.py and the #415 truncated-file tests pass locally on the rebased branch.


Generated with Claude Code

@PeterStaar-IBM PeterStaar-IBM changed the title fix: report why a document failed to load, so a password is told from a damaged file feat: report why a document failed to load, so a password is told from a damaged file Oct 26, 2026
@PeterStaar-IBM
PeterStaar-IBM merged commit 3317576 into docling-project:main Oct 27, 2026
43 checks passed
@wittjeff
wittjeff deleted the fix/load-failure-reason branch October 27, 2026 12:46
wittjeff added a commit to wittjeff/docling-parse that referenced this pull request Oct 27, 2026
Resolve the document.h conflict with docling-project#414: keep the load_error fields and the lazy-annotations comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jeff Witt <1848307+wittjeff@users.noreply.github.com>
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.

2 participants