Skip to content

addcontent hangs when default fallback is enabled causing multilingual contentlets to be re submitted#35030

Merged
dario-daza merged 31 commits intomainfrom
35029-addcontent-hangs-when-default-fallback-is-enabled-causing-multilingual-contentlets-to-be-re-submitted
Mar 30, 2026
Merged

addcontent hangs when default fallback is enabled causing multilingual contentlets to be re submitted#35030
dario-daza merged 31 commits intomainfrom
35029-addcontent-hangs-when-default-fallback-is-enabled-causing-multilingual-contentlets-to-be-re-submitted

Conversation

@dario-daza
Copy link
Copy Markdown
Member

@dario-daza dario-daza commented Mar 18, 2026

Fix 1 — addContent() hangs with multilingual content

File: dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPIImpl.java

Context

When DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE=true, render() returns mixed-language content. When the client re-submits it to addContent(), the language-scoped DELETE left fallback-language entries in multi_tree. copyMultiTree() then hit a duplicate on INSERT and threw IllegalArgumentException, rolling back the transaction and hanging the client indefinitely.

Proposed Changes

  • overridesMultitreesByPersonalization(): Added a defaultContentToDefaultLanguage boolean gate. When true, performs a full DELETE (all entries for the page/personalization/variant) instead of the language-scoped DELETE, ensuring a clean state before re-inserting.
  • copyMultiTree(): replaced the IllegalArgumentException on duplicate detection with a debug log + skip as defence-in-depth.

Fix 2 — entity.containers.uuid includes wrong-language contentlets

File: dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageRenderUtil.java

Context

When DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE=true, an ESP-only contentlet (e.g. content3) added to the page was appearing in entity.containers.uuid when rendering the ENG page. The rendered HTML was correct (content3 absent), creating an inconsistency that confuses headless/EMA consumers of the JSON response.

Proposed Changes

  • getContentletOrFallback(): removed a findContentletByIdentifierAnyLanguage fallback that was too broad. After findContentletByIdentifierOrFallback returns empty (no version in requested language or default language), the method now returns null, correctly excluding the contentlet from the response. The contract is now strictly:
    requested language → default language → exclude.

Relationship between the two fixes

Both stem from the same root: DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE=true causes the page to surface content from multiple languages simultaneously, and the rest of the stack wasn't accounting for that. Fix 1 protects the write path (addContent), Fix 2 corrects the read path (render).

UI test

Here is a test in the UI with 3 languages, the same happens if we have content even in more languages.

multiple-language-content-test.mp4

Fixes

This PR fixes: #35029

This PR fixes: #35029

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Mar 18, 2026

Claude finished @dario-daza's task in 4m 37s —— View job


PR Review

  • Gather context
  • Read changed files
  • Analyze for bugs, risks, edge cases
  • Post review

Issues

1. null multiTrees: exception reinstated despite reviewer request
MultiTreeAPIImpl.java:700 — The PR restores throw new DotDataException("empty list passed in"), reversing the earlier Logger.debug + return change. Reviewer fabrizzio-dotCMS explicitly asked for log-and-return; the author marked it "rollback to older code not in scope", yet the current code throws. Both reviewers approved anyway. The behavior is consistent with saveMultiTrees() at line 624 (which also throws), so correctness isn't affected — but it contradicts the review discussion and could still break callers that pass null expecting a no-op. Fix this →

2. Silent duplicate skip could hide incomplete deletes
MultiTreeAPIImpl.java:869 — The copyMultiTree duplicate-skip is documented as "defense-in-depth", but if the new language-pair DELETE SQL has an edge case that misses rows, those duplicates will be silently dropped (DEBUG only). No WARN, no pageId in the log. In production, this makes it invisible that the page ended up with fewer contentlets than submitted. Consider logging at WARN with pageId and the skipped contentlet's identifier so incidents are traceable.

3. getContainerAsID() vs castToOriginalContainerId() consistency
PageResourceHelper.java:710 — The map is keyed using mt.getContainerAsID(), which parses the folder path locally via FileAssetContainerUtil. The lookup path (buildMultiTreeLookupKey) resolves via APILocator.getContainerAPI().getLiveContainerByFolderPath(...). For entries already in the DB, parent2 should be a UUID, so isFolderAssetContainerId will be false and both paths return the same value. Still, if a multi_tree row were ever stored with a path-based container ID, the keys would diverge. Low probability, but worth a comment or a shared resolution utility.

4. SELECT_EXISTING_MULTI_TREE_KEYS_BY_PAGE_AND_CHILDREN — unbounded IN clause
MultiTreeAPIImpl.java:97-99 — The IN clause is built with one ? per contentlet in multiTrees. For a typical page this is fine, but there is no guard against extremely large pages. If a page ever has hundreds of containers × contentlets, this query could become problematic. Not a current bug, but worth noting.

5. overridesMultitreesByPersonalizationDEFAULT_CONTENT_TO_DEFAULT_LANGUAGE=true + no language provided falls through to full delete
MultiTreeAPIImpl.java:746-755 — When the flag is true but languageIdOpt is empty, the code falls into the full DELETE_ALL_MULTI_TREE_SQL_BY_RELATION_AND_PERSONALIZATION branch, wiping all languages. The flag is intentionally gated on languageIdOpt.isPresent(), so this is probably correct (no-language context = legacy path). The behavior difference between the two flag states under this path is not documented. A comment at the else-branch clarifying the intent would prevent future regressions.

6. getContentletOrFallback — Time Machine triple lookup
PageRenderUtil.java:731-748 — When Time Machine is active and both the variant-aware and live findContentletByIdentifierOrFallback calls return empty, execution falls through to the third call at line 746 (the non-TM path). Three DB calls for the same contentlet identifier on a cache miss. Pre-existing behavior, not introduced here, but visible in this diff.


Minor

  • DELETE_ALL_MULTI_TREE_BY_RELATION_AND_PERSONALIZATION_PER_TWO_LANGUAGES_NOT_SQL — trailing space before =? on the last param (language_id =?) is inconsistent with language_id = ? just before it. Cosmetic but looks like a typo (MultiTreeAPIImpl.java:96).
  • The buildMultiTreeKey Javadoc says @param container — The container identifier ({@code parent2} column), but the parameter is named container while in loadExistingMultiTreeKeys it's read from row.get("parent2"). Accurate but could align parameter name with the column name for clarity.

Overall: fixes are well-targeted and the SQL logic for the language-pair delete is correct. Test coverage for the three-language case is solid. The silent-skip on duplicate in copyMultiTree is the main thing to watch in production.

@dario-daza dario-daza changed the title 35029 addcontent hangs when default fallback is enabled causing multilingual contentlets to be re submitted addcontent hangs when default fallback is enabled causing multilingual contentlets to be re submitted Mar 18, 2026
@dario-daza dario-daza marked this pull request as ready for review March 19, 2026 19:08
nollymar
nollymar previously approved these changes Mar 25, 2026
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Mar 25, 2026

Pull Request Unsafe to Rollback!!!


Category: M-4 — OSGi Plugin API Breakage

  • Risk Level: 🟡 MEDIUM
  • Why it's unsafe: The MultiTreeAPI interface lives in com.dotmarketing.factories, which is listed in dotCMS/src/main/resources/osgi/osgi-extra.conf as an OSGi-exported package. Two method overloads had their Optional<Long> languageIdOpt parameter replaced with a primitive long languageId. Any OSGi plugin compiled against N-1's Optional<Long> signature will encounter a NoSuchMethodError if those methods are invoked on N-1 after rolling back from N.
  • Code that makes it unsafe:
    • dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPI.java — both overridesMultitreesByPersonalization overloads changed signature from final Optional<Long> languageIdOpt to final long languageId
    • dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPIImpl.java — matching implementation changed accordingly
  • Alternative (if possible): Add the new long languageId overload alongside the existing Optional<Long> overload rather than replacing it. Deprecate the old overload and remove it in a later release once N-2 is outside the rollback window. This allows plugins compiled against either signature to coexist safely.

Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM
  • Why it's unsafe: PageRenderUtil.getContentletOrFallback() previously included a last-resort fallback to findContentletByIdentifierAnyLanguage(), which could surface contentlets from any language (when the content type allows languageFallback()). That fallback has been removed. Under DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE=true, the entity.containers.uuid field in the page render JSON response will now exclude contentlets that exist only in a third language (not the requested or default language). Headless / EMA consumers that have adapted to the corrected behavior after N is deployed will see those contentlets reappear in the response if the system is rolled back to N-1.
  • Code that makes it unsafe:
    • dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageRenderUtil.java lines 747–770 — removed the findContentletByIdentifierAnyLanguage fallback block and replaced it with return contentletOpt.orElse(null)
  • Alternative (if possible): Gate the new exclusion behavior behind a feature flag (distinct from the existing DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE flag) so N-1 can be deployed without headless consumers observing the behavioral regression.

ihoffmann-dot
ihoffmann-dot previously approved these changes Mar 25, 2026
…-is-enabled-causing-multilingual-contentlets-to-be-re-submitted
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Mar 27, 2026

Pull Request Unsafe to Rollback!!!


Category: M-4 — OSGi Plugin API Breakage

  • Risk Level: 🟡 MEDIUM
  • Why it's unsafe: The PageLoader class lives in com.dotcms.rendering.velocity.services, which is exported in dotCMS/src/main/resources/osgi/osgi-extra.conf (line 384). Its public buildStream() method had its signature changed — the old 3-argument form buildStream(IHTMLPage, PageMode, String) was replaced with a 4-argument form buildStream(IHTMLPage, PageMode, String, long viewingLanguageId). Any OSGi plugin compiled against N-1 that calls buildStream(IHTMLPage, PageMode, String) will encounter a NoSuchMethodError if it runs against N-1 after rolling back from N.
  • Code that makes it unsafe:
    • dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageLoader.javabuildStream() signature changed from (IHTMLPage, PageMode, String) to (IHTMLPage, PageMode, String, long)
    • dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageLoader.java — private addWidgetPreExecuteCodeAndPageInfo() also changed signature (lower risk since private, but same pattern)
  • Alternative (if possible): Add the new 4-argument overload alongside the existing 3-argument overload. In the old overload, derive the viewingLanguageId from the page (as the original code did). Deprecate the old overload and remove it in a later release once N-2 is outside the rollback window.

Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM
  • Why it's unsafe: PageRenderUtil.getContentletOrFallback() previously included a last-resort fallback to findContentletByIdentifierAnyLanguage() that could surface contentlets from any language when the content type allows languageFallback(). That fallback has been removed. Under DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE=true, the entity.containers.uuid field in the page render JSON response will now exclude contentlets that exist only in a third language (not the requested or default language). Headless / EMA consumers that adapted to the corrected behavior after N is deployed will see those contentlets reappear in the response if the system is rolled back to N-1.
  • Code that makes it unsafe:
    • dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageRenderUtil.java lines 747–770 — removed the findContentletByIdentifierAnyLanguage fallback block and replaced it with return contentletOpt.orElse(null)
  • Alternative (if possible): Gate the new exclusion behavior behind a feature flag (distinct from the existing DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE flag) so N-1 can be deployed without headless consumers observing the behavioral regression.

@dario-daza
Copy link
Copy Markdown
Member Author

dario-daza commented Mar 27, 2026

Pull Request Unsafe to Rollback!!!

Category: M-4 — OSGi Plugin API Breakage

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: The PageLoader class lives in com.dotcms.rendering.velocity.services, which is exported in dotCMS/src/main/resources/osgi/osgi-extra.conf (line 384). Its public buildStream() method had its signature changed — the old 3-argument form buildStream(IHTMLPage, PageMode, String) was replaced with a 4-argument form buildStream(IHTMLPage, PageMode, String, long viewingLanguageId). Any OSGi plugin compiled against N-1 that calls buildStream(IHTMLPage, PageMode, String) will encounter a NoSuchMethodError if it runs against N-1 after rolling back from N.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageLoader.javabuildStream() signature changed from (IHTMLPage, PageMode, String) to (IHTMLPage, PageMode, String, long)
    • dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageLoader.java — private addWidgetPreExecuteCodeAndPageInfo() also changed signature (lower risk since private, but same pattern)
  • Alternative (if possible): Add the new 4-argument overload alongside the existing 3-argument overload. In the old overload, derive the viewingLanguageId from the page (as the original code did). Deprecate the old overload and remove it in a later release once N-2 is outside the rollback window.

Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: PageRenderUtil.getContentletOrFallback() previously included a last-resort fallback to findContentletByIdentifierAnyLanguage() that could surface contentlets from any language when the content type allows languageFallback(). That fallback has been removed. Under DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE=true, the entity.containers.uuid field in the page render JSON response will now exclude contentlets that exist only in a third language (not the requested or default language). Headless / EMA consumers that adapted to the corrected behavior after N is deployed will see those contentlets reappear in the response if the system is rolled back to N-1.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageRenderUtil.java lines 747–770 — removed the findContentletByIdentifierAnyLanguage fallback block and replaced it with return contentletOpt.orElse(null)
  • Alternative (if possible): Gate the new exclusion behavior behind a feature flag (distinct from the existing DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE flag) so N-1 can be deployed without headless consumers observing the behavioral regression.

Regarding to the rollback scenario Category: M-3 — REST / Headless API Contract Change:

A rollback from N to N-1 restores the buggy behavior, not a feature. Any headless consumer that "adapted to the corrected behavior" after N is deployed was adapting to correct, spec-compliant output. Restoring the bug on rollback would re-expose third-language content leaking into the response — an outcome no intentional API design would document.

The existing DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE flag is already the appropriate gate.

Regarding to the rollback scenario Category: M-4 — OSGi Plugin API Breakage

Solved.

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Mar 27, 2026

Pull Request Unsafe to Rollback!!!


Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM
  • Why it's unsafe: PageRenderUtil.getContentletOrFallback() previously fell back to findContentletByIdentifierAnyLanguage() when a contentlet had no version in the requested or default language and its content type allowed languageFallback(). That fallback has been removed. Under DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE=true, contentlets that exist only in a third language (not the requested or default language) are now excluded from the entity.containers.uuid field in the page render JSON response. Any headless/EMA consumer that adapted to the corrected (N) behavior — expecting those contentlets to be absent — will see them reappear if the system is rolled back to N-1.
  • Code that makes it unsafe:
    • dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageRenderUtil.java — lines 747–770: removed the findContentletByIdentifierAnyLanguage fallback block and replaced it with return contentletOpt.orElse(null)
  • Alternative (if possible): Gate the new exclusion behavior behind a separate feature flag so that N-1 operators can manually disable it after rollback if their headless consumers have already adapted to N's behavior. The existing DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE flag could serve this purpose but changes its semantics when toggled post-rollback.

Note on previously flagged M-4 (OSGi Plugin API Breakage): The concern from earlier analysis reviews is now resolved in the current commit (a5cca650):

  • MultiTreeAPI.java interface still uses Optional<Long> languageIdOpt in both overridesMultitreesByPersonalization overloads — no signature change.
  • PageLoader.java added the new 4-arg buildStream(IHTMLPage, PageMode, String, long) alongside the preserved 3-arg overload, which delegates to it. Both the old and new signatures coexist in the exported com.dotcms.rendering.velocity.services OSGi package.

@dario-daza
Copy link
Copy Markdown
Member Author

Pull Request Unsafe to Rollback!!!

Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: PageRenderUtil.getContentletOrFallback() previously fell back to findContentletByIdentifierAnyLanguage() when a contentlet had no version in the requested or default language and its content type allowed languageFallback(). That fallback has been removed. Under DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE=true, contentlets that exist only in a third language (not the requested or default language) are now excluded from the entity.containers.uuid field in the page render JSON response. Any headless/EMA consumer that adapted to the corrected (N) behavior — expecting those contentlets to be absent — will see them reappear if the system is rolled back to N-1.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageRenderUtil.java — lines 747–770: removed the findContentletByIdentifierAnyLanguage fallback block and replaced it with return contentletOpt.orElse(null)
  • Alternative (if possible): Gate the new exclusion behavior behind a separate feature flag so that N-1 operators can manually disable it after rollback if their headless consumers have already adapted to N's behavior. The existing DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE flag could serve this purpose but changes its semantics when toggled post-rollback.

Note on previously flagged M-4 (OSGi Plugin API Breakage): The concern from earlier analysis reviews is now resolved in the current commit (a5cca650):

  • MultiTreeAPI.java interface still uses Optional<Long> languageIdOpt in both overridesMultitreesByPersonalization overloads — no signature change.
  • PageLoader.java added the new 4-arg buildStream(IHTMLPage, PageMode, String, long) alongside the preserved 3-arg overload, which delegates to it. Both the old and new signatures coexist in the exported com.dotcms.rendering.velocity.services OSGi package.

Regarding to the rollback scenario Category: M-3 — REST / Headless API Contract Change:

A rollback from N to N-1 restores the buggy behavior, not a feature. Any headless consumer that "adapted to the corrected behavior" after N is deployed was adapting to correct, spec-compliant output. Restoring the bug on rollback would re-expose third-language content leaking into the response — an outcome no intentional API design would document.

The existing DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE flag is already the appropriate gate.

@dario-daza dario-daza enabled auto-merge March 30, 2026 17:44
@dario-daza dario-daza added this pull request to the merge queue Mar 30, 2026
Merged via the queue into main with commit 0dfcd6e Mar 30, 2026
48 checks passed
@dario-daza dario-daza deleted the 35029-addcontent-hangs-when-default-fallback-is-enabled-causing-multilingual-contentlets-to-be-re-submitted branch March 30, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[BUG]: addContent hangs when DEFAULT_CONTENT_TO_DEFAULT_LANGUAGE=true causing multilingual contentlets to be re-submitted

4 participants