Hardening: a single unserializable field aborts indexing of the whole contentlet
@fabrizzio-dotCMS is already working on this.
Since Sep 8, 2026.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Hardening, not a live bug. There is no known path on trunk that reaches this today —
#37272 removed the one trigger that did. What remains is the shape of the error handling, which
costs the whole document whenever any future per-field failure occurs. Filed as
Type : Refactoringfor that reason; if a live trigger turns up it should be re-labelled a
defect.
Problem Statement
ESMappingAPIImpl.loadFields wraps the entire per-field loop body in one catch that logs at WARN and rethrows:
// ESMappingAPIImpl.java:1143-1147
} catch (Exception e) {
Logger.warn(ESMappingAPIImpl.class, "Error indexing field: " + field.getFieldName()
+ " of contentlet: " + contentlet.getInode(), e);
throw new DotDataException(e.getMessage(), e);
}
Rethrowing aborts toMap, so one field failing removes the entire contentlet from the index — every other field of that contentlet included. The content is in the database and invisible to search, with a single WARN as the only record.
The failure is also lost from both engines at once, in every migration phase: the document is rendered once and shared across providers (ContentletIndexAPIImpl.mapContentletForProcessor — "Compute mapping once; reuse across all providers for the same contentlet"), so the throw happens before the fan-out. It is not an Elasticsearch-only or OpenSearch-only defect.
The Date branch a few lines above already shows the intended shape — it catches, degrades to toString(), and keeps going.
Severity: Medium. No known trigger on trunk after #37272, but the structure means any future per-field failure costs the whole document rather than the field.
Relationship to #37272
#37272 fixed one specific trigger: a TextField on a numeric column handed a String to DecimalFormat.format(). That path no longer reaches this catch — loadNumericField converts, or omits the field and logs a WARN.
This issue is the surrounding structure, which #37272 deliberately left alone (see its spec's non-goals). Other paths still reach it: a corrupt Date, ESUtils.sha256 throwing on a unique field, unreadable binary metadata, a Category with a dangling reference.
Steps to Reproduce
No supported path reproduces this on trunk today — that is why it is filed as a structural defect rather than an active bug. It is reachable by any field whose serialization throws something the branch does not anticipate.
Historical reproduction, from #37272 before the fix:
- Create a content type with a Text field backed by an
integerNcolumn (legal —TextField.acceptedDataTypes()includesINTEGER, andhtmlpageasset.sortOrderships that way). - Get a non-numeric
Stringinto that field without going through the coercing save path (the starter import does this;ImportStarterUtilwrites via the factory with no validation). - Index the contentlet.
- Observed: the whole contentlet is absent from the index, with one WARN naming the field.
Expected vs actual
| Expected | Actual | |
|---|---|---|
| Contentlet with one unserializable field | indexed, that field degraded or omitted | entirely absent from the index |
| Other fields of that contentlet | searchable | all lost |
| Log level when the document is aborted | ERROR | WARN |
| Log level when the document survives | WARN | n/a — it never survives |
Acceptance Criteria
- A field whose serialization fails unexpectedly is skipped; the remaining fields of that contentlet are still emitted and the document is present in the index.
- That failure is logged at ERROR, naming the field and the content type (not only the contentlet inode), so it is actionable and visible to monitoring. No WARN-then-rethrow remains.
- The known, anticipated degradation introduced by #37272 (a non-numeric value in a numeric column) keeps logging at WARN — the two tiers are distinguishable: WARN for a degradation we expect and tolerate, ERROR for one we did not anticipate.
- No WARN or ERROR on the happy path.
loadFieldsruns for every field of every contentlet on every index write; logging there would flood the log on every page'ssortOrder. - Correctly-stored values of every field type emit identical map entries — no document for existing content moves. Same constraint #37272 worked under.
Two things to settle before touching the catch
1. Who depends on document-complete-or-absent? This converts a loud failure (document missing, somebody notices) into a quiet one (document present, one field short). For a search index a partial document beats no document, but it is a contract change for anything that assumes a present document is complete. Enumerate those callers first.
2. What is the test seam? This is the reason the work was not folded into #37272. Forcing an unexpected serialization failure in an arbitrary field has no obvious seam — it needs static mocking or a production hook that exists only for the test. The naturally-occurring candidates are either already handled (the Date branch degrades) or need hand-corrupted data. Decide the seam before writing the fix: changing how indexing fails, in the hottest path in indexing, without a test that demonstrates it is not acceptable.
Scope
- In: the per-field catch at
ESMappingAPIImpl.java:1143-1147and its log levels. - Out: the numeric-column conversion (done in #37272); the reindex-failure reporting UI;
ImportStarterUtilhardening (named as a known gap in #37272's spec, still unfixed).
dotCMS Version
main, after #37272's fix.
Links
- #37272 — the specific trigger, fixed; this is the structure it left in place
- Spec non-goals:
specs/37272-textfield-numeric-column/spec.md
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.