opendataloader-project/opendataloader-pdf · error · InvalidPdfFileException
'{}' is not a valid PDF file (missing %PDF- header).
Error message
'{}' is not a valid PDF file (missing %PDF- header). What it means
InvalidPdfFileException thrown by validatePdfMagicNumber before veraPDF is ever invoked: the first 1024 bytes of the file do not contain the ASCII %PDF- marker. This is the 'not a PDF at all' failure mode, distinct from error 81 (header present, body broken). Detected cheaply so the cost of a full veraPDF parse is avoided on obviously wrong input.
Source
Thrown at java/opendataloader-pdf-core/src/main/java/org/opendataloader/pdf/processors/DocumentProcessor.java:694
*
* <p>ISO 32000-1 §7.5.2 allows the {@code %PDF-} header to appear "near
* the beginning" of the file rather than strictly at byte 0; real-world
* PDFs sometimes have a leading UTF-8 BOM or whitespace. A 1024-byte
* search window matches that tolerance while still rejecting any
* JPG/PNG/HTML/empty file.
*
* @throws InvalidPdfFileException if the magic number is not present
* @throws IOException if the file cannot be opened or read
*/
private static void validatePdfMagicNumber(String pdfName) throws IOException {
Path path = Path.of(pdfName);
byte[] head;
try (InputStream in = Files.newInputStream(path)) {
head = in.readNBytes(1024);
}
byte[] marker = "%PDF-".getBytes(StandardCharsets.US_ASCII);
if (indexOfBytes(head, marker) < 0) {
throw new InvalidPdfFileException(
"'" + displayName(pdfName) + "' is not a valid PDF file (missing %PDF- header).");
}
}
/**
* Verifies the JVM temporary directory is writable.
*
* <p>veraPDF streams anything larger than a small in-memory threshold
* through a temporary file: the Standard 14 font metrics embedded in the
* jar, embedded font programs, CMaps and decoded content streams all take
* that path. A PDF stream has no size bound while memory does, so writing
* to disk is by design and cannot be avoided by buffering.
*
* <p>When the temporary directory is not writable those reads fail deep
* inside veraPDF, where the failure is logged at {@code FINE} and
* swallowed. Processing then continues on missing data and surfaces as an
* unrelated {@code NullPointerException}, or — worse — completes with
* exit code 0 while silently dropping most of the text. Failing up frontView on GitHub (pinned to a7789b8e77)
Solutions
- Confirm the file is actually a PDF: check the first bytes (head -c 5 file.pdf should print %PDF-) or run file(1).
- Re-export or re-save the source document as PDF from the originating application.
- If accepting user uploads, validate the magic number on your side before calling processFile and reject early with your own message.
- Check the path is not pointing at a directory or a symlink to a non-PDF target.
Example fix
# before: file is a renamed image $ mv scan.jpg report.pdf && odl-pdf report.pdf -> InvalidPdfFileException: 'report.pdf' is not a valid PDF file (missing %PDF- header). # after: export to real PDF $ file report.pdf # -> PDF document, version 1.4 $ odl-pdf report.pdf
Defensive patterns
Strategy: validation
Validate before calling
// Validate the magic number yourself before calling the library:
private static boolean looksLikePdf(Path p) throws IOException {
try (InputStream in = Files.newInputStream(p)) {
byte[] head = in.readNBytes(5);
byte[] marker = "%PDF-".getBytes(StandardCharsets.US_ASCII);
return Arrays.equals(head, marker);
}
} Type guard
static boolean isMissingHeader(IOException e) {
return e instanceof InvalidPdfFileException
&& e.getMessage() != null
&& e.getMessage().contains("missing %PDF- header");
} Try / catch
try {
DocumentProcessor.processFile(pdfName, config);
} catch (InvalidPdfFileException e) {
if (e.getMessage() != null && e.getMessage().contains("missing %PDF- header")) {
rejectUpload("File is not a PDF (no %PDF- header).");
} else {
throw e; // corrupted-content variant (error 81)
}
} Prevention
- Check the first 5 bytes for %PDF- on user uploads before queuing for conversion.
- Use the `file` command or a mime-type check in your ingest layer.
- Reject non-PDF files with your own message rather than letting the library surface it mid-pipeline.
When it happens
Trigger: Any public entry point (DocumentProcessor.processFile/extractContents/preprocessing/processFileWithResult, OpenDataLoaderPDF.processFile, AutoTagger.tag) is called with a path whose content lacks %PDF- in the first 1024 bytes — e.g. a JPEG/PNG/HTML/ZIP/text file, an empty file, or a PostScript file.
Common situations: A user renamed a .jpg/.png/.html to .pdf. The wrong file was passed (a .txt report instead of the PDF). An empty placeholder file. A path pointing at an OpenDocument or Office file exported to the wrong format.
Related errors
AI-assisted analysis of opendataloader-project/opendataloader-pdf@a7789b8e77 (2026-08-14).
Data as JSON: /api/errors/3e1bf833221aa287.
Report an issue: GitHub.