opendataloader-project/opendataloader-pdf · error · IOException

Page image fetcher returned null for page

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.

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.

Appendix: 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)