{"record":{"id":"66130d954a8cb4e2","repo":"spring-projects/spring-ai","slug":"unreasonable-pdf-paragraph-depth","errorCode":null,"errorMessage":"Unreasonable pdf paragraph depth","messagePattern":"Unreasonable pdf paragraph depth","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":87,"sourceCode":"\t\t\t\t\tthis.document.getDocumentCatalog().getDocumentOutline(), 0, new HashSet<>());\n\t\t}\n\t\tcatch (Exception e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\n\t}\n\n\tpublic List<Paragraph> flatten() {\n\t\tList<Paragraph> paragraphs = new ArrayList<>();\n\t\tfor (var child : this.rootParagraph.children()) {\n\t\t\tflatten(child, paragraphs, 0);\n\t\t}\n\t\treturn paragraphs;\n\t}\n\n\tprivate void flatten(Paragraph current, List<Paragraph> paragraphs, int depth) {\n\t\tif (depth > REASONABLE_DEPTH) {\n\t\t\tthrow new IllegalStateException(\"Unreasonable pdf paragraph depth\");\n\t\t}\n\t\tparagraphs.add(current);\n\t\tfor (var child : current.children()) {\n\t\t\tflatten(child, paragraphs, depth + 1);\n\t\t}\n\t}\n\n\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.","sourceCodeStart":69,"sourceCodeEnd":105,"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#L69-L105","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Inspect the PDF's bookmark/outline structure (e.g. with PDFBox) and flatten or normalize overly deep nesting before feeding it to the reader.","Re-generate the PDF with a flattened outline (e.g. print-to-PDF or a tool that removes/relevels bookmarks).","If deep nesting is legitimate, adjust REASONABLE_DEPTH or preprocess the outline yourself rather than relying on this reader.","Catch IllegalStateException and reject/skip the offending document in batch pipelines."],"exampleFix":"// before\nDocumentReader reader = new ParagraphPdfDocumentReader(resource);\nList<Document> docs = reader.get(); // throws on deep outline\n\n// after\nList<Document> docs;\ntry {\n    docs = new ParagraphPdfDocumentReader(resource).get();\n} catch (IllegalStateException e) {\n    // fall back to page-based reader or sanitize outline first\n    docs = new PagePdfDocumentReader(resource).get();\n}","handlingStrategy":"try-catch","validationCode":"try (PDDocument doc = Loader.loadPDF(file)) {\n    PDOutlineItem root = doc.getDocumentCatalog().getDocumentOutline() != null\n        ? doc.getDocumentCatalog().getDocumentOutline().getFirstChild() : null;\n    int maxDepth = 0;\n    for (PDOutlineItem it = root; it != null; it = it.getNextSibling()) {\n        int d = 1; PDOutlineItem c = it.getFirstChild();\n        Deque<Object[]> stack = new ArrayDeque<>();\n        if (c != null) stack.push(new Object[]{c, 1});\n        while (!stack.isEmpty()) {\n            Object[] e = stack.pop();\n            PDOutlineItem cur = (PDOutlineItem) e[0]; int lvl = (Integer) e[1];\n            maxDepth = Math.max(maxDepth, lvl);\n            for (PDOutlineItem ch = cur.getFirstChild(); ch != null; ch = ch.getNextSibling())\n                stack.push(new Object[]{ch, lvl + 1});\n        }\n    }\n    if (maxDepth > 100) throw new IllegalStateException(\"outline too deep: \" + maxDepth);\n}","typeGuard":null,"tryCatchPattern":"try {\n    List<Document> docs = new ParagraphPdfDocumentReader(resource).get();\n} catch (IllegalStateException e) {\n    if (e.getMessage().contains(\"Unreasonable pdf paragraph depth\")) {\n        docs = new PagePdfDocumentReader(resource).get(); // fallback\n    } else throw e;\n}","preventionTips":["Sanitize untrusted PDFs (strip or flatten outlines) before paragraph-based extraction","Prefer PagePdfDocumentReader when you don't need outline-derived paragraph structure","Fence deep-outline documents out at ingest with a PDFBox depth scan","Handle all DocumentReader.get() IllegalStateExceptions in batch pipelines"],"tags":["pdf","recursion-depth","document-reader","malformed-input"],"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"}