Summary
A ClassCastException is thrown during page rendering when a contentlet with an intentionally empty binary field is loaded as related content at depth ≥ 1. The error surfaces as a DotDataException: null (the null is the empty exception message from the unwrapped ClassCastException).
Steps to Reproduce
- Create a Content Type with one or more binary fields (e.g.
productImage3).
- Create a contentlet of that type and leave the binary field empty (no file uploaded).
- Relate that contentlet to another contentlet rendered on a page.
- Request the Page API with
depth=1 (or higher): GET /api/v1/page/json/{page}?depth=1
- Observe the error in the logs.
Expected Behavior
When a binary field has no file, the field should be treated as absent — no metadata generation should be attempted, and no exception should be thrown.
Actual Behavior
WARN strategy.DefaultTransformStrategy - An error occurred when retrieving the Binary file
from field 'productImage3' in Contentlet with ID 'e04a7278f90d941a45fc7f8b0294e489': null
Caused by: java.lang.ClassCastException
at com.dotmarketing.portlets.contentlet.model.Contentlet.getBinary(Contentlet.java:1204)
...
at com.dotcms.storage.FileMetadataAPIImpl.internalGetGenerateMetadata(FileMetadataAPIImpl.java:379)
at io.vavr.control.Try.getOrElseThrow(Try.java:748)
at com.dotmarketing.portlets.contentlet.transform.strategy.DefaultTransformStrategy.addBinaries(DefaultTransformStrategy.java:263)
com.dotmarketing.exception.DotDataException: null
Root Cause
A double-transform problem when a binary field is empty.
Step-by-step breakdown
1. First transform — BinaryViewStrategy (via hydratedContentMapTransformer, triggered by the hardcoded hydrateRelated=true in ContentUtils.addRelationships)
// ContentHelper.java:432-435
if (hydrateRelated) {
final DotContentletTransformer myTransformer = new DotTransformerBuilder()
.hydratedContentMapTransformer().content(contentlet).build();
contentlet = myTransformer.hydrate().get(0); // BinaryViewStrategy runs here
}
BinaryViewStrategy.transform calls getBinaryMetadata("productImage3") → returns null (no file, no metadata). Because AVOID_MAP_SUFFIX_FOR_VIEWS is present, sufix = "", and it unconditionally writes:
// BinaryViewStrategy.java:62
map.put(field.variable() + sufix, transform(field, contentlet));
// transform() returns emptyMap() when metadata is null
// result: contentlet.map["productImage3"] = Collections.emptyMap()
The contentlet map now has "productImage3" → emptyMap().
2. Second transform — DefaultTransformStrategy.addBinaries (via contentResourceOptions with BINARIES)
// ContentHelper.java:442
final Map<String, Object> map = ContentletUtil.getContentPrintableMap(user, contentlet, ...);
addBinaries is called on the same (now mutated) contentlet:
contentlet.get("productImage3") → emptyMap() → non-null → passes the guard at FileMetadataAPIImpl:338
retrieveMetaData(...) → null (no stored metadata — no file was ever attached)
- Falls through to
Try.of(() -> generateContentletMetadata(contentlet)).getOrElseThrow(DotDataException::new)
generateBasicMetadata → contentlet.getBinary("productImage3")
// Contentlet.java:1204 — CRASH
File f = (File) map.get(velocityVarName);
// map.get("productImage3") = emptyMap() → ClassCastException (no message → DotDataException: null)
Why re-saving the contentlet does not fix it
Re-saving without attaching a file creates a new inode but skips metadata generation (generateBasicMetadata checks file.exists() at line 149 and silently skips empty fields). The entire chain replays identically on the next render.
Affected Files
| File |
Line |
Issue |
Contentlet.java |
1204 |
Unchecked (File) cast — no instanceof guard |
BinaryViewStrategy.java |
62 |
Puts emptyMap() at the binary field key when metadata is null and AVOID_MAP_SUFFIX_FOR_VIEWS is set |
FileMetadataAPIImpl.java |
338 |
Null-guard passes for any non-null value, including emptyMap() |
Proposed Fix
Option A — Guard the cast in Contentlet.getBinary (defensive, catches any future poisoning):
// Contentlet.java:1203
public java.io.File getBinary(String velocityVarName) throws IOException {
final Object rawValue = map.get(velocityVarName);
File f = (rawValue instanceof File) ? (File) rawValue : null;
if ((f == null || !f.exists())) {
// existing filesystem lookup logic ...
Option B — Fix the root cause in BinaryViewStrategy.transform (don't write to the map when there is no metadata):
// BinaryViewStrategy.java:57-67
final Map<String, Object> binaryMap = transform(field, contentlet);
if (!binaryMap.isEmpty()) {
map.put(field.variable() + sufix, binaryMap);
// ... existing AVOID_MAP_SUFFIX_FOR_VIEWS logic
}
Both options should be applied together.
Workarounds (until fix is shipped)
- Upload a placeholder file to the empty binary field. This causes metadata to be generated and stored, so
retrieveMetaData returns a result and the crash path is never reached. ⚠️ The file must remain permanently — removing it and re-saving restores the broken state.
- Request the page at
depth=0. This returns only identifiers for related content and skips contentletToJSON with hydrateRelated=true, avoiding the double-transform entirely.
Severity
- Visible impact: The WARN is non-fatal — the page renders, but
productImage3MetaData and all /dA/ links for that field are absent from the JSON response.
- Frequency: Affects any contentlet with an intentionally empty binary field used as related content at depth ≥ 1.
Links
https://helpdesk.dotcms.com/a/tickets/34967
Summary
A
ClassCastExceptionis thrown during page rendering when a contentlet with an intentionally empty binary field is loaded as related content at depth ≥ 1. The error surfaces as aDotDataException: null(thenullis the empty exception message from the unwrappedClassCastException).Steps to Reproduce
productImage3).depth=1(or higher):GET /api/v1/page/json/{page}?depth=1Expected Behavior
When a binary field has no file, the field should be treated as absent — no metadata generation should be attempted, and no exception should be thrown.
Actual Behavior
Root Cause
A double-transform problem when a binary field is empty.
Step-by-step breakdown
1. First transform —
BinaryViewStrategy(viahydratedContentMapTransformer, triggered by the hardcodedhydrateRelated=trueinContentUtils.addRelationships)BinaryViewStrategy.transformcallsgetBinaryMetadata("productImage3")→ returnsnull(no file, no metadata). BecauseAVOID_MAP_SUFFIX_FOR_VIEWSis present,sufix = "", and it unconditionally writes:The contentlet map now has
"productImage3" → emptyMap().2. Second transform —
DefaultTransformStrategy.addBinaries(viacontentResourceOptionswithBINARIES)addBinariesis called on the same (now mutated) contentlet:contentlet.get("productImage3")→emptyMap()→ non-null → passes the guard atFileMetadataAPIImpl:338retrieveMetaData(...)→null(no stored metadata — no file was ever attached)Try.of(() -> generateContentletMetadata(contentlet)).getOrElseThrow(DotDataException::new)generateBasicMetadata→contentlet.getBinary("productImage3")Why re-saving the contentlet does not fix it
Re-saving without attaching a file creates a new inode but skips metadata generation (
generateBasicMetadatachecksfile.exists()at line 149 and silently skips empty fields). The entire chain replays identically on the next render.Affected Files
Contentlet.java(File)cast — noinstanceofguardBinaryViewStrategy.javaemptyMap()at the binary field key when metadata is null andAVOID_MAP_SUFFIX_FOR_VIEWSis setFileMetadataAPIImpl.javaemptyMap()Proposed Fix
Option A — Guard the cast in
Contentlet.getBinary(defensive, catches any future poisoning):Option B — Fix the root cause in
BinaryViewStrategy.transform(don't write to the map when there is no metadata):Both options should be applied together.
Workarounds (until fix is shipped)
retrieveMetaDatareturns a result and the crash path is never reached.depth=0. This returns only identifiers for related content and skipscontentletToJSONwithhydrateRelated=true, avoiding the double-transform entirely.Severity
productImage3MetaDataand all/dA/links for that field are absent from the JSON response.Links
https://helpdesk.dotcms.com/a/tickets/34967