ADR 0156 — A file whose bytes are not its format is an outcome, not a failure
- Status: Accepted
- Date: 2026-11-08
- Amends: ADR 0040 — parse errors caused by bytes that are not the format leave the real failures and become remembered outcomes
- Related: ADR 0005 (oversize and undecodable sources stay failures), ADR 0155 (the HTML errors of the same run), indexing and retrieval
Context
Section titled “Context”The same ingest of 37 projects as ADR 0155 reported, besides the HTML templates, these errors from test fixtures, and the run exited 1:
pdf parse failed: Stream has ended unexpectedlyforinvoice1_document.pdf, whose 5 bytes aretest\n, andpdf parse failed: Cannot read an empty filefor the 0-bytecontract_dummy.pdf. pypdf also loggedinvalid pdf header: b'test\n'and threeEOF marker not foundwarnings. With no logging handler configured, Python’s last-resort handler printed them to stderr, over the progress line.cannot identify image file <_io.BytesIO object at 0x134231760>fortest-txt-as-png.png, whose bytes areI am not an image, and for a 0-byteicon.pngfixture. Pillow’sUnidentifiedImageErrorleft the extractor unwrapped, with a memory address in its message.invalid JSON: Expecting value: line 1 column 1 (char 0)for twelve 0-byte snippet fixtures in the Shopware platform’s tests, andinvalid JSON: Expecting property name enclosed in double quotesforentity-schema-invalid.json, a deliberately broken PHPStan fixture.
ADR 0040 made a document without text an outcome but kept “parse errors” among the real failures. Each of these errors is decided by the file’s bytes alone. Every run read the files, failed, and ended Indexing Incomplete again, which is what ADR 0040 rejected for scans: one unreadable file fails every run forever, which hides real failures. In this run three real limits (ingest.max_file_bytes on two chat exports and a 2 MB data file) were listed among about 140 errors.
Two tests added on 2026-10-28, when JSON with comments became readable, asserted that malformed JSON still fails: test_invalid_json_still_fails_at_its_line and test_malformed_json_still_reports_an_error. They guarded that the JSONC pass did not hide a syntax error. No record gave a reason for failing on one.
Decision
Section titled “Decision”Bytes that are not the format are an outcome. An extractor raises UnreadableDocument, a subclass of ExtractionError, when the format’s parser rejects the bytes as not being that format:
- PDF: pypdf’s
PdfReadErrorfamily, includingEmptyFileErrorandPdfStreamError, as not a readable PDF: <pypdf’s message>. The family also holdsFileNotDecryptedError: a password-protected PDF is reported as encrypted PDF: it opens only with its password, since Imprint has no way to supply one and no later run could read it either. - Images: Pillow cannot identify the format (not a readable image: unrecognized format), or decoding fails with
OSErrororSyntaxError(truncated or corrupt data). The image is decoded before OCR runs, so an OCR error is never taken for damage.
Ingest and refresh treat it as ADR 0040’s nothing to index: the file is listed under No text with its reason, recorded per content hash and extraction profile in data/extraction-results.sqlite3 so that later runs do not read it, and the run succeeds. A file that had chunks keeps them. Single-file ingestion reports Nothing to index. --force reads it again.
JSON that does not parse is text. When a .json or .jsonc file parses neither as JSON nor as JSONC, it is indexed verbatim as code, with source_format: invalid_json and the parser’s message in json_error. A source file with a syntax error is still source: .ts, .yaml and .toml files are indexed without being parsed, and tsconfig*.json is already kept verbatim. An empty JSON file has no text, so ADR 0040 applies.
Everything else stays a failure. Other parser exceptions (a pypdf error on a file it recognized as a PDF, an OCR runtime error, Pillow’s decompression-bomb refusal), unavailable extractors, read errors, undecodable text and the size limits ingest.max_file_bytes and ingest.max_doc_size_mb still keep prior chunks and make the run incomplete, as ADR 0005 and ADR 0040 decided. Callers that catch ExtractionError (ingest-url, desktop export learning) see UnreadableDocument as before.
pypdf’s warnings are not printed. The PDF extractor sets the pypdf logger to ERROR. Those warnings report damage pypdf recovers from; what it cannot recover from arrives as an exception.
Rejected alternatives
Section titled “Rejected alternatives”- Keep failing, as the 2026-10-28 tests asserted. The run above shows the cost: the same fixture errors on every run, an exit code of 1, and the size-limit failures that need the owner’s action buried among them.
- Skip without remembering. Every run would read and parse the files again and list them again. ADR 0040 rejected the same for scans.
- Treat every extractor exception as unreadable. A parser bug on a valid but unusual file, or a broken OCR runtime, would be recorded as a property of the file and not retried after the library is fixed, because parser versions are not part of the extraction profile. Only the parsers’ own “this is not the format” errors qualify.
- Skip malformed JSON like a broken PDF. It is readable text, and a fixture’s content is what a search for it should find.
Consequences
Section titled “Consequences”- On that run’s projects, the PDF, image and empty JSON fixtures become No text entries that later runs skip,
entity-schema-invalid.jsonis indexed, nothing is printed over the progress line, and only the three size-limit files remain failures. - Parser versions are not part of the extraction profile. A PDF that pypdf rejects stays recorded after a pypdf upgrade that might read it, until the file or the profile changes or
--forceruns. - The
no_texttable ofdata/extraction-results.sqlite3now also holds these reasons. Its schema is unchanged. - Desktop export learning indexes an export JSON file that does not parse verbatim, where it used to report Extraction failed.
Invalidation conditions
Section titled “Invalidation conditions”- If a parser upgrade starts reading files it rejected as not being its format, then add parser versions to the extraction profile so recorded results are retried.
- If Pillow raises
OSErrorwhile decoding for a reason in the environment rather than in the file (a decoder missing from its build), then such images are recorded wrongly and the image rule must be narrowed. - If verbatim malformed JSON fills search with large broken exports, then cap or skip it.
Validation
Section titled “Validation”tests/test_malformed_documents.py(PdfTests,ImageTests): non-PDF and empty bytes are unreadable and log no pypdf warning; a password-protected PDF is unreadable with its own reason; another pypdf error stays anExtractionError; text and nothing under an image name, and a truncated PNG, are unreadable; an OCR error on a valid image propagates.tests/test_json_doc.pyandtests/test_json_config_ingestion.py: malformed JSON is kept verbatim with its error line, and an empty file has no text.tests/test_projects.py(NoTextDocumentTests): an unreadable document completes the run with its reason and is not read on the next run, and a single unreadable file is Nothing to index. These tests fail on the previous extractors.- Run on macOS with the installed runtime’s Python 3.12 (pypdf 6.21.2, Pillow 13.0.0) against the Shopware fixtures named above: each produced the error quoted in the context before the change, and an outcome and no stderr output after it.
- Not run: a complete
imprint ingestof the 37 projects, and Windows and Linux. The change is in pure Python with no platform-specific path handling.