Skip to content

fix: return 400 instead of 500 for corrupt document containers - #594

Open
frey0-0 wants to merge 1 commit into
Unstructured-IO:mainfrom
frey0-0:fix/content-parse-errors-4xx
Open

frey0-0 wants to merge 1 commit into
Unstructured-IO:mainfrom
frey0-0:fix/content-parse-errors-4xx

Conversation

@frey0-0

@frey0-0 frey0-0 commented Oct 28, 2026 •

Copy link
Copy Markdown

Problem

Deterministic content-parse failures on corrupt uploads currently return HTTP 500 instead of a 4xx:

  • truncated/corrupt OLE files (.doc, .ppt, .msg): olefile.olefile.OleFileError (e.g. OLE directory index out of range, incomplete OLE sector)
  • truncated/corrupt OOXML packages (.pptx etc.): zipfile.BadZipFile (File is not a zip file)
  • invalid spreadsheets: unstructured.errors.UnprocessableEntityError (Not a valid XLSX file.)

These escape the except OSError / except ValueError handlers in pipeline_api and fall through to the app-level catch-all in app.py, which returns 500 {"detail": str(e)}.

This matters for downstream consumers that classify failures by status class: 4xx means "bad input, don't retry", 5xx means "transient, retry". A corrupt document that can never parse gets retried as if the server were at fault, and user-facing errors get mislabeled as server failures.

Root cause

The app-level Exception handler in app.py is the only net for these exception types; nothing in pipeline_api maps them to client errors.

Fix

Add three type-based exception handlers before the existing except OSError block in pipeline_api (ordering matters: OleFileError subclasses OSError, so it must be caught first):

  • olefile.olefile.OleFileError -> 400 (File does not appear to be a valid OLE file: ...)
  • zipfile.BadZipFile -> 400 (File does not appear to be a valid OOXML file: ...)
  • unstructured.errors.UnprocessableEntityError -> 400 (detail passes through)

olefile is added as an explicit dependency in pyproject.toml since it is now imported directly (it was previously only transitive via unstructured). Note the submodule import (from olefile.olefile import OleFileError): olefile 0.47 does not re-export the exception classes at the package level.

What still returns 500

Kept deliberately out of scope, flagged here for maintainers:

  • ValueError: bytes length not a multiple of item size (from array/struct inside olefile on degenerate OLE files) - it escapes OleFileIO as a bare ValueError, which partition() also raises for many legitimate reasons, so catching it by type is not safe and an allowlist of message substrings would be brittle.
  • 'NoneType' object has no attribute 'inner_content_elements' from python-docx on broken .docx files - an AttributeError, not cleanly catchable by type. (Truncated .docx files already return 422 via the existing not a ZIP archive (so not a DOCX file) ValueError branch, so the common docx truncation case is covered.)

Everything else (genuine server errors, OOM, model failures) continues to return 500.

Test evidence

test_general/api/test_app.py::test_truncated_binary_documents_return_400 uploads half of sample-docs/fake.doc (OLE) and half of sample-docs/fake-power-point.pptx (OOXML zip) and asserts 400. Both tests fail against main (500 from the catch-all) and pass with this change. Ran locally with uv run pytest test_general/api/test_app.py -k truncated: 2 passed.

View guided diff

Deterministic parse failures on corrupt uploads (truncated OLE files
raising olefile.olefile.OleFileError, truncated OOXML packages raising
zipfile.BadZipFile, invalid spreadsheets raising
unstructured.errors.UnprocessableEntityError) were falling through the
OSError/ValueError handlers to the app-level catch-all and returning
500. Since olefile's OleFileError subclasses OSError, the new clause
must run before the OSError handler.

These are client-side problems: 400 with a descriptive detail, so
callers can classify them as bad input rather than transient server
errors and retry.

Co-Authored-By: Claude Code <noreply@anthropic.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.

1 participant