opendataloader-project/opendataloader-pdf · error · IOException

Page image fetcher returned null for page %s

Error message

Page image fetcher returned null for page %s

What it means

MemoryPageImageCache.getOrFetch() delegates to a PageImageFetcher callback when a page image is not in the cache. If the fetcher returns null (rather than throwing an exception), the cache treats it as a programming error — a null BufferedImage cannot be cached or processed downstream. This guard prevents a silent NPE later in the pipeline when code tries to use the image.

Source

Thrown at java/opendataloader-pdf-core/src/main/java/org/opendataloader/pdf/hybrid/MemoryPageImageCache.java:37

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * In-memory page image cache. Stores images in a HashMap; evict() removes
 * entries so GC can reclaim memory (~25MB per page image).
 */
public class MemoryPageImageCache implements PageImageCache {

    private final Map<Integer, BufferedImage> cache = new HashMap<>();

    @Override
    public BufferedImage getOrFetch(int pageIndex, PageImageFetcher fetcher) throws IOException {
        BufferedImage image = cache.get(pageIndex);
        if (image == null) {
            image = fetcher.fetch(pageIndex);
            if (image == null) {
                throw new IOException("Page image fetcher returned null for page " + pageIndex);
            }
            cache.put(pageIndex, image);
        }
        return image;
    }

    @Override
    public void evict(int pageIndex) {
        cache.remove(pageIndex);
    }

    @Override
    public void close() {
        cache.clear();
    }
}

View on GitHub (pinned to a7789b8e77)

Solutions

  1. Fix the PageImageFetcher implementation to throw IOException on failure instead of returning null.
  2. Wrap the fetcher: `cache.getOrFetch(pageIndex, idx -> { BufferedImage img = rawFetcher.fetch(idx); if (img == null) throw new IOException("Render returned null for page " + idx); return img; })`.
  3. Check whether the page is renderable before calling getOrFetch (e.g., verify the page has content and a valid media box).
  4. Log the pageIndex to identify which page triggers the null — often a specific corrupt page.

Example fix

// before: fetcher returns null on render failure, cache throws opaque error
BufferedImage img = cache.getOrFetch(pageIndex, idx -> {
    return renderer.renderImage(idx); // returns null on error
});

// after: fetcher throws IOException on null, cache stays happy
BufferedImage img = cache.getOrFetch(pageIndex, idx -> {
    BufferedImage rendered = renderer.renderImage(idx);
    if (rendered == null) {
        throw new IOException("Renderer returned null for page " + idx);
    }
    return rendered;
});
Defensive patterns

Strategy: validation

Validate before calling

// Wrap the fetcher to guarantee it never returns null
PageImageFetcher safeFetcher = pageIndex -> {
    BufferedImage img = rawFetcher.fetch(pageIndex);
    if (img == null) {
        throw new IOException("Render returned null for page " + pageIndex);
    }
    return img;
};
BufferedImage image = cache.getOrFetch(pageIndex, safeFetcher);

Type guard

public static boolean isNonNullFetcher(PageImageFetcher fetcher) {
    return fetcher != null;
    // Cannot statically guarantee fetch() returns non-null — must wrap at runtime.
}

Try / catch

try {
    image = cache.getOrFetch(pageIndex, safeFetcher);
} catch (IOException e) {
    if (e.getMessage().contains("returned null")) {
        // Fetcher implementation bug — fix the fetcher, don't retry with same fetcher
        LOGGER.severe("PageImageFetcher returned null for page " + pageIndex
            + ". Fix the fetcher implementation to throw IOException on failure.");
        throw e;
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling cache.getOrFetch(pageIndex, fetcher) where the fetcher implementation's fetch() method returns null instead of throwing an IOException on failure. This typically happens when a fetcher wraps a rendering call that returns null on error (e.g., PDFBox renderImageWithDPI returning null in some edge cases) without translating the null into an exception.

Common situations: Custom PageImageFetcher implementation that silently swallows rendering errors and returns null; PDFBox rendering a page with an unsupported color space or corrupt content stream where the renderer returns null rather than throwing; a fetcher that delegates to another cache layer which has been evicted/closed and returns null; a fetcher that hits an error path but forgets to throw.

Related errors


AI-assisted analysis of opendataloader-project/opendataloader-pdf@a7789b8e77 (2026-08-14). Data as JSON: /api/errors/8975382ec6f4b5d3. Report an issue: GitHub.