Skip to content

ClassCastException when rendering page with empty binary field in related contentlet #35188

@dsilvam

Description

@dsilvam

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

  1. Create a Content Type with one or more binary fields (e.g. productImage3).
  2. Create a contentlet of that type and leave the binary field empty (no file uploaded).
  3. Relate that contentlet to another contentlet rendered on a page.
  4. Request the Page API with depth=1 (or higher): GET /api/v1/page/json/{page}?depth=1
  5. 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)
  • generateBasicMetadatacontentlet.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)

  1. 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.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    In Review

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions