Hi,
We found a bug that occurs when comparing two DOCX documents with many identical tables. We encountered this with customer data, which we have no control over, so we can’t work around this.
TL;DR
Comparing two DOCX docs that have 1,536 or more tables in common produces the following exception:
com.groupdocs.comparison.common.exceptions.ComparisonException: java.lang.IllegalStateException: Unexpected node type '5'.
at com.groupdocs.comparison.Comparer.compare(Unknown Source)
Caused by: java.lang.IllegalStateException: Unexpected node type '5'.
at com.groupdocs.comparison.internal.c.a.w.Av.bQ(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.Yo.cTi(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.PG.cTk(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.PG.cTj(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.PG.xnS(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.Yo.xNH(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.Yo.g(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.UQ.xDM(Unknown Source)
...
Reproduction
Code and sample docs that reproduce this (100% AI, but works):
groupdocs-rca-repro.zip (22.6 KB)
And here’s an AI root cause analysis (can also be found in the archive):
Root cause analysis: IllegalStateException: Unexpected node type '5' when comparing DOCX documents with many identical tables
Product: GroupDocs.Comparison for Java
Versions affected: 25.2 and 26.5 (latest as of this report) — verified on both; the relevant code is identical
Runtime: Java 21 (Eclipse Temurin), Linux x86_64, metered license
Severity: Comparison fails deterministically for affected document pairs; no workaround preserves Word Track Changes output
Summary
Comparing two DOCX documents fails with
com.groupdocs.comparison.common.exceptions.ComparisonException:
java.lang.IllegalStateException: Unexpected node type '5'.
whenever the two documents contain more than 1,535 tables whose text content is identical across both documents. The failure is deterministic and content-driven. The root cause is an unbounded index in the internal “equal tables” marker-character scheme, which overflows the reserved private-use character range and corrupts the diff engine’s text-position-to-node mapping.
We hit this in production on a real document pair (~1,080 pages, ~38,000 paragraphs, 1,697 tables per document, of which 1,685 have byte-identical text across the two revisions). The attached synthetic repro reproduces it with generated content only, and pins the failure threshold exactly at 1,536 equal tables.
Reproduction
| Files | Equal tables | Result on 25.2 and 26.5 |
|---|---|---|
control1500_src.docx / control1500_tgt.docx |
1,500 | Compares successfully |
t1535_src.docx / t1535_tgt.docx |
1,535 | Compares successfully |
t1536_src.docx / t1536_tgt.docx |
1,536 | Fails: Unexpected node type ‘5’ |
overflow1600_src.docx / overflow1600_tgt.docx |
1,600 | Fails: Unexpected node type ‘5’ |
Each pair is identical except for one changed paragraph at the top; each document contains N single-cell tables with distinct generated text (Unique table content number 00000 …), so all N tables are “equal tables” between the two documents.
Steps (harness attached as Repro.java; reads the license keys from GROUPDOCS_PUBLIC_KEY / GROUPDOCS_PRIVATE_KEY environment variables):
try (Comparer comparer = new Comparer("t1536_src.docx")) {
comparer.add("t1536_tgt.docx");
CompareOptions options = new CompareOptions();
options.setWordTrackChanges(true);
options.setDetectStyleChanges(true);
comparer.compare("out.docx", options);
}
Note: a valid license is required to reproduce — evaluation mode truncates the documents below the threshold before comparison.
Observed stack trace (25.2; identical failure signature on 26.5):
com.groupdocs.comparison.common.exceptions.ComparisonException: java.lang.IllegalStateException: Unexpected node type '5'.
at com.groupdocs.comparison.Comparer.compare(Unknown Source)
Caused by: java.lang.IllegalStateException: Unexpected node type '5'.
at com.groupdocs.comparison.internal.c.a.w.Av.bQ(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.Yo.cTi(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.PG.cTk(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.PG.cTj(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.PG.xnS(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.Yo.xNH(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.Yo.g(Unknown Source)
at com.groupdocs.comparison.internal.c.a.w.UQ.xDM(Unknown Source)
...
(Class names are the obfuscated names in the 25.2 artifact; they may differ per release.)
Root cause
The Word comparison engine flattens each document into a character stream in which structural elements are represented by marker characters (cell, field, shape, footnote, etc.). Tables are handled specially: a pre-pass (UQ, debug label "{Equal tables list}") collects every table whose getText() occurs in both documents and assigns it a sequential index:
// UQ (25.2) — map fills with one entry per distinct equal-table text, unbounded
if (yb2.abjO.containsKey(string)) continue;
yb2.abjO.set(string, yb2.abjO.getCount() + 1);
During flattening (BV.bS, node type 5 = Table), the marker character for such a table is computed as (char)('\uF400' + index) with no upper bound:
// arN
static String P(char c, int n) { return Character.toString((char)(c + n)); }
But the corresponding recognizer only accepts a 1,536-character window:
// arN — table-marker predicate
static boolean iF(char c) { return '\uF400' <= c && c < '\uFA00'; }
Since indices start at 1, tables with index ≥ 1,536 receive characters U+FA00 and above. That range is the CJK Compatibility Ideographs block, for which Character.isLetter() returns true — so the character classifier (arN.iD) treats these table markers as ordinary letter characters instead of structural markers.
The two character streams still match (both documents contain the same overflowed character), so the diff records them as part of an “equal text” run. When the merge phase replays that run, Yo.cTi requires the current node on both sides to be a text Run (node type 21) but finds the actual Table node (node type 5) that the misclassified marker stands for, and throws:
// Yo.cTi — merge handler for matched text segments
if (this.xnW().getNodeType() != 21) { Av.bQ(this.xnW()); } // throws "Unexpected node type '5'."
The observed threshold (1,535 equal tables OK, 1,536 fails) matches this arithmetic exactly: index 1,536 → '\uF400' + 1536 = U+FA00, the first character outside iF’s range.
Additional observations
CompareOptions.setWordTrackChanges(false)avoids the failing code path: the same document pair compares successfully in that mode. This is not an acceptable workaround for us, because we need genuine Word Track Changes (w:ins/w:del) output.- The bug is unrelated to page count, file size, table nesting, or table placement; only the number of distinct table texts shared by both documents matters.
- Verified present in 26.5 by inspection of the same routines (identical range check and unbounded index assignment) and by running the affected production pair against 26.5.