spring-projects/spring-ai · error · IllegalStateException

Unreasonable pdf paragraph depth

Error message

Unreasonable pdf paragraph depth

What it means

ParagraphManager.flatten() recursively flattens the PDF outline-derived paragraph tree into a flat list. If the nesting depth exceeds REASONABLE_DEPTH, the tree is considered pathologically deep (likely malformed or malicious input) and an IllegalStateException is thrown instead of risking stack overflow or unbounded recursion.

Source

Thrown at document-readers/spring-ai-pdf-document-reader/src/main/java/org/springframework/ai/reader/pdf/config/ParagraphManager.java:87

					this.document.getDocumentCatalog().getDocumentOutline(), 0, new HashSet<>());
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}

	}

	public List<Paragraph> flatten() {
		List<Paragraph> paragraphs = new ArrayList<>();
		for (var child : this.rootParagraph.children()) {
			flatten(child, paragraphs, 0);
		}
		return paragraphs;
	}

	private void flatten(Paragraph current, List<Paragraph> paragraphs, int depth) {
		if (depth > REASONABLE_DEPTH) {
			throw new IllegalStateException("Unreasonable pdf paragraph depth");
		}
		paragraphs.add(current);
		for (var child : current.children()) {
			flatten(child, paragraphs, depth + 1);
		}
	}

	/**
	 * For given {@link PDOutlineNode} bookmark convert all sibling {@link PDOutlineItem}
	 * items into {@link Paragraph} instances under the parentParagraph. For each
	 * {@link PDOutlineItem} item, recursively call
	 * {@link ParagraphManager#generateParagraphs} to process its children items.
	 * @param parentParagraph Root paragraph that the bookmark sibling items should be
	 * added to.
	 * @param bookmark TOC paragraphs to process.
	 * @param level Current TOC deepness level.
	 * @param visited used to prevent infinite recursion
	 * @return Returns a tree of {@link Paragraph}s that represent the PDF document TOC.

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Inspect the PDF's bookmark/outline structure (e.g. with PDFBox) and flatten or normalize overly deep nesting before feeding it to the reader.
  2. Re-generate the PDF with a flattened outline (e.g. print-to-PDF or a tool that removes/relevels bookmarks).
  3. If deep nesting is legitimate, adjust REASONABLE_DEPTH or preprocess the outline yourself rather than relying on this reader.
  4. Catch IllegalStateException and reject/skip the offending document in batch pipelines.

Example fix

// before
DocumentReader reader = new ParagraphPdfDocumentReader(resource);
List<Document> docs = reader.get(); // throws on deep outline

// after
List<Document> docs;
try {
    docs = new ParagraphPdfDocumentReader(resource).get();
} catch (IllegalStateException e) {
    // fall back to page-based reader or sanitize outline first
    docs = new PagePdfDocumentReader(resource).get();
}
Defensive patterns

Strategy: try-catch

Validate before calling

try (PDDocument doc = Loader.loadPDF(file)) {
    PDOutlineItem root = doc.getDocumentCatalog().getDocumentOutline() != null
        ? doc.getDocumentCatalog().getDocumentOutline().getFirstChild() : null;
    int maxDepth = 0;
    for (PDOutlineItem it = root; it != null; it = it.getNextSibling()) {
        int d = 1; PDOutlineItem c = it.getFirstChild();
        Deque<Object[]> stack = new ArrayDeque<>();
        if (c != null) stack.push(new Object[]{c, 1});
        while (!stack.isEmpty()) {
            Object[] e = stack.pop();
            PDOutlineItem cur = (PDOutlineItem) e[0]; int lvl = (Integer) e[1];
            maxDepth = Math.max(maxDepth, lvl);
            for (PDOutlineItem ch = cur.getFirstChild(); ch != null; ch = ch.getNextSibling())
                stack.push(new Object[]{ch, lvl + 1});
        }
    }
    if (maxDepth > 100) throw new IllegalStateException("outline too deep: " + maxDepth);
}

Try / catch

try {
    List<Document> docs = new ParagraphPdfDocumentReader(resource).get();
} catch (IllegalStateException e) {
    if (e.getMessage().contains("Unreasonable pdf paragraph depth")) {
        docs = new PagePdfDocumentReader(resource).get(); // fallback
    } else throw e;
}

Prevention

When it happens

Trigger: Calling DocumentReader.load()/ParagraphManager.getParagraphs() on a PDF whose outline (bookmarks) hierarchy nests deeper than REASONABLE_DEPTH; the exception is raised during the recursive flatten(child, paragraphs, depth + 1) walk.

Common situations: Processing untrusted or auto-generated PDFs with deeply nested bookmark trees; documents produced by tools that emit thousands of nested outline entries; adversarial PDFs crafted to cause deep recursion (DoS).

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/66130d954a8cb4e2. Report an issue: GitHub.