spring-projects/spring-ai · error · IllegalStateException
pdf containing circular reference or unreasonable nesting le
Error message
pdf containing circular reference or unreasonable nesting level
What it means
generateParagraphs() walks the PDF outline (PDOutlineNode) recursively while tracking visited COSDictionary objects. It throws IllegalStateException when the outline nesting level exceeds REASONABLE_DEPTH or when an outline item's COS object is revisited, which means the PDF contains a circular bookmark reference or unreasonably deep outline.
Source
Thrown at document-readers/spring-ai-pdf-document-reader/src/main/java/org/springframework/ai/reader/pdf/config/ParagraphManager.java:113
/**
* 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.
* @throws IOException
*/
protected Paragraph generateParagraphs(Paragraph parentParagraph, PDOutlineNode bookmark, Integer level,
Set<COSDictionary> visited) throws IOException {
PDOutlineItem current = bookmark.getFirstChild();
if (level > REASONABLE_DEPTH || (current != null && !visited.add(current.getCOSObject()))) {
throw new IllegalStateException("pdf containing circular reference or unreasonable nesting level");
}
while (current != null) {
int pageNumber = getPageNumber(current);
var nextSiblingNumber = getPageNumber(current.getNextSibling());
if (nextSiblingNumber < 0) {
nextSiblingNumber = getPageNumber(current.getLastChild());
}
var paragraphPosition = (current.getDestination() instanceof PDPageXYZDestination)
? ((PDPageXYZDestination) current.getDestination()).getTop() : 0;
var currentParagraph = new Paragraph(parentParagraph, current.getTitle(), level, pageNumber,
nextSiblingNumber, paragraphPosition);
parentParagraph.children().add(currentParagraph);
View on GitHub (pinned to 98a7beda4f)
Solutions
- Sanitize the PDF outline before reading (rebuild or strip /Outlines with a tool like qpdf, pikepdf, or PDFBox).
- Open the PDF in a viewer to confirm the bookmark tree is cyclic/deep, then re-export the document without the problematic outline.
- Catch IllegalStateException and fall back to PagePdfDocumentReader which ignores the outline.
- If the depth is legitimate for your corpus, raise REASONABLE_DEPTH in ParagraphManager.
Example fix
// before
DocumentReader reader = new ParagraphPdfDocumentReader(resource);
List<Document> docs = reader.get();
// after
List<Document> docs;
try {
docs = new ParagraphPdfDocumentReader(resource).get();
} catch (IllegalStateException e) {
docs = new PagePdfDocumentReader(resource).get(); // outline-free fallback
} Defensive patterns
Strategy: try-catch
Validate before calling
try (PDDocument doc = Loader.loadPDF(file)) {
PDOutline outline = doc.getDocumentCatalog().getDocumentOutline();
if (outline == null) return;
Set<COSDictionary> seen = new HashSet<>();
Deque<PDOutlineItem> stack = new ArrayDeque<>();
for (PDOutlineItem it = outline.getFirstChild(); it != null; it = it.getNextSibling())
stack.push(it);
while (!stack.isEmpty()) {
PDOutlineItem cur = stack.pop();
if (!seen.add(cur.getCOSObject()))
throw new IllegalStateException("circular outline reference detected");
for (PDOutlineItem ch = cur.getFirstChild(); ch != null; ch = ch.getNextSibling())
stack.push(ch);
}
} Try / catch
try {
List<Document> docs = new ParagraphPdfDocumentReader(resource).get();
} catch (IllegalStateException e) {
if (e.getMessage().contains("circular reference")) {
log.warn("PDF {} has cyclic outline; using page reader", resource);
docs = new PagePdfDocumentReader(resource).get();
} else throw e;
} Prevention
- Detect cyclic outline items (repeated COSDictionary on a path) with PDFBox before loading
- Rewrite untrusted PDFs with qpdf or pikepdf to drop malformed /Outlines
- Fall back to PagePdfDocumentReader when outline integrity is uncertain
- Treat IllegalArgumentException/IllegalStateException from readers as a per-document skip signal in bulk ingestion
When it happens
Trigger: Loading a PDF via ParagraphPdfDocumentReader where the document outline has a bookmark whose COSDictionary appears twice on a path (cycle) or whose level exceeds REASONABLE_DEPTH; recursion from ParagraphManager constructor and from nested generateParagraphs calls.
Common situations: Corrupted or hand-edited PDFs with cyclic /Outlines entries; maliciously crafted documents targeting PDF viewers/parsers; PDFs generated by buggy producers that link outline items back to ancestors.
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
- Unreasonable pdf paragraph depth
- Unreasonable number of lines (%d) computed from content of p
- Skipping paragraph titled '<title>' because it has an invali
- Line length cannot be negative
- Unreasonable lineLength of %d provided
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/bca832c747564d44.
Report an issue: GitHub.