dotCMS / dotCMS/core

Hardening: a single unserializable field aborts indexing of the whole contentlet

Open
#37,462 0 comments 0 reactions 1 assignee View on GitHub

@fabrizzio-dotCMS is already working on this.

Since Sep 8, 2026.

OpenSearch Team : Scout Type : Refactoring
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 : Refactoring for 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:

  1. Create a content type with a Text field backed by an integerN column (legal — TextField.acceptedDataTypes() includes INTEGER, and htmlpageasset.sortOrder ships that way).
  2. Get a non-numeric String into that field without going through the coercing save path (the starter import does this; ImportStarterUtil writes via the factory with no validation).
  3. Index the contentlet.
  4. 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. loadFields runs for every field of every contentlet on every index write; logging there would flood the log on every page's sortOrder.
  • 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-1147 and its log levels.
  • Out: the numeric-column conversion (done in #37272); the reindex-failure reporting UI; ImportStarterUtil hardening (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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.