{"record":{"id":"bca832c747564d44","repo":"spring-projects/spring-ai","slug":"pdf-containing-circular-reference-or-unreasonable","errorCode":null,"errorMessage":"pdf containing circular reference or unreasonable nesting level","messagePattern":"pdf containing circular reference or unreasonable nesting level","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"document-readers/spring-ai-pdf-document-reader/src/main/java/org/springframework/ai/reader/pdf/config/ParagraphManager.java","lineNumber":113,"sourceCode":"\t/**\n\t * For given {@link PDOutlineNode} bookmark convert all sibling {@link PDOutlineItem}\n\t * items into {@link Paragraph} instances under the parentParagraph. For each\n\t * {@link PDOutlineItem} item, recursively call\n\t * {@link ParagraphManager#generateParagraphs} to process its children items.\n\t * @param parentParagraph Root paragraph that the bookmark sibling items should be\n\t * added to.\n\t * @param bookmark TOC paragraphs to process.\n\t * @param level Current TOC deepness level.\n\t * @param visited used to prevent infinite recursion\n\t * @return Returns a tree of {@link Paragraph}s that represent the PDF document TOC.\n\t * @throws IOException\n\t */\n\tprotected Paragraph generateParagraphs(Paragraph parentParagraph, PDOutlineNode bookmark, Integer level,\n\t\t\tSet<COSDictionary> visited) throws IOException {\n\n\t\tPDOutlineItem current = bookmark.getFirstChild();\n\t\tif (level > REASONABLE_DEPTH || (current != null && !visited.add(current.getCOSObject()))) {\n\t\t\tthrow new IllegalStateException(\"pdf containing circular reference or unreasonable nesting level\");\n\t\t}\n\n\t\twhile (current != null) {\n\n\t\t\tint pageNumber = getPageNumber(current);\n\t\t\tvar nextSiblingNumber = getPageNumber(current.getNextSibling());\n\t\t\tif (nextSiblingNumber < 0) {\n\t\t\t\tnextSiblingNumber = getPageNumber(current.getLastChild());\n\t\t\t}\n\n\t\t\tvar paragraphPosition = (current.getDestination() instanceof PDPageXYZDestination)\n\t\t\t\t\t? ((PDPageXYZDestination) current.getDestination()).getTop() : 0;\n\n\t\t\tvar currentParagraph = new Paragraph(parentParagraph, current.getTitle(), level, pageNumber,\n\t\t\t\t\tnextSiblingNumber, paragraphPosition);\n\n\t\t\tparentParagraph.children().add(currentParagraph);\n","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/document-readers/spring-ai-pdf-document-reader/src/main/java/org/springframework/ai/reader/pdf/config/ParagraphManager.java#L95-L131","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nDocumentReader reader = new ParagraphPdfDocumentReader(resource);\nList<Document> docs = reader.get();\n\n// after\nList<Document> docs;\ntry {\n    docs = new ParagraphPdfDocumentReader(resource).get();\n} catch (IllegalStateException e) {\n    docs = new PagePdfDocumentReader(resource).get(); // outline-free fallback\n}","handlingStrategy":"try-catch","validationCode":"try (PDDocument doc = Loader.loadPDF(file)) {\n    PDOutline outline = doc.getDocumentCatalog().getDocumentOutline();\n    if (outline == null) return;\n    Set<COSDictionary> seen = new HashSet<>();\n    Deque<PDOutlineItem> stack = new ArrayDeque<>();\n    for (PDOutlineItem it = outline.getFirstChild(); it != null; it = it.getNextSibling())\n        stack.push(it);\n    while (!stack.isEmpty()) {\n        PDOutlineItem cur = stack.pop();\n        if (!seen.add(cur.getCOSObject()))\n            throw new IllegalStateException(\"circular outline reference detected\");\n        for (PDOutlineItem ch = cur.getFirstChild(); ch != null; ch = ch.getNextSibling())\n            stack.push(ch);\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    List<Document> docs = new ParagraphPdfDocumentReader(resource).get();\n} catch (IllegalStateException e) {\n    if (e.getMessage().contains(\"circular reference\")) {\n        log.warn(\"PDF {} has cyclic outline; using page reader\", resource);\n        docs = new PagePdfDocumentReader(resource).get();\n    } else throw e;\n}","preventionTips":["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"],"tags":["pdf","circular-reference","outline","document-reader"],"backgroundTag":"internal-invariant-violation","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}