Skip to content

feat(content): suppress synthetic URL fallback for regular contentlets#35584

Open
fabrizzio-dotCMS wants to merge 9 commits into
mainfrom
feat/suppress-content-url-fallback-35583
Open

feat(content): suppress synthetic URL fallback for regular contentlets#35584
fabrizzio-dotCMS wants to merge 9 commits into
mainfrom
feat/suppress-content-url-fallback-35583

Conversation

@fabrizzio-dotCMS
Copy link
Copy Markdown
Member

Summary

  • Adds FEATURE_FLAG_SUPPRESS_CONTENT_URL_FALLBACK (default ON) to ContentHelper.getUrl(Contentlet).
  • When ON, regular contentlets (not Page, FileAsset, or dotAsset) return url: "" instead of the auto-computed /content.{uuid} identifier URI that is meaningless to API consumers.
  • HTMLPage, FileAsset, and dotAsset types continue resolving their URL from the identifier — unaffected.
  • Flag OFF fully restores legacy behaviour.

Closes #35583

Changes

File What changed
FeatureFlagName.java New constant FEATURE_FLAG_SUPPRESS_CONTENT_URL_FALLBACK with javadoc
ContentHelper.java Lazy<Boolean> flag field + guard in getUrl(Contentlet); @VisibleForTesting setter
ContentHelperTest.java 4 new unit tests covering the full flag × content-type matrix

Test plan

  • Flag ON + regular contentlet (no URL field) → url == ""
  • Flag ON + regular contentlet (with URL field) → stored URL returned unchanged
  • Flag OFF + regular contentlet → identifier-based fallback runs (legacy)
  • Flag ON + HTMLPage → identifier URL hydrated as today

🤖 Generated with Claude Code

#35583)

Adds FEATURE_FLAG_SUPPRESS_CONTENT_URL_FALLBACK (default ON) to ContentHelper.getUrl()
so that regular contentlets without an explicit URL field return "" instead of the
auto-computed /content.{uuid} identifier URI that is not meaningful to API consumers.
HTMLPage, FileAsset, and dotAsset types continue to resolve their URL from the
identifier as before. Unit tests cover all four matrix cases from the acceptance criteria.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 5, 2026

Claude finished @fabrizzio-dotCMS's task in 2m 38s —— View job


PR Review

  • Read changed files
  • Analyze logic and edge cases
  • Check tests
  • Post review

Overall the change is small and well-scoped. A few things worth a second look:

1. PR description vs. actual behavior — null vs ""

The summary says regular contentlets "return url: """, but ContentHelper.getUrl(Contentlet) actually returns null (ContentHelper.java:216). All callers (ContentletToMapTransformer.java:108-113, DefaultTransformStrategy.java:136-141) skip map.put on null, so the key is omitted entirely — not present-but-empty. Tests confirm this (assertFalse(transformed.containsKey(URL_FIELD))). Please correct the PR description; "returned as empty string" vs. "field absent" matter to API consumers.

2. Default-on flag is a silent breaking change

This is essentially a default-on schema change for any REST endpoint that hydrates contentlets via the transformer pipeline. Evidence in this very PR:

  • ContentToolTest.test_pullHydrated_BlogContentlet had to drop containsKey("url") and assert containsKey("hostName") instead — the new assertion does not verify what the original was checking; it only proves some property is added. Consider asserting the absence of url explicitly or removing the assertion entirely.
  • AI.postman_collection.json removed a pm.expect(result).to.have.property('url') check, confirming external consumers' contracts shift here.

External clients with the same expectation will break without warning. If this is intentional and signed off, fine — but a release note / migration callout is warranted.

3. Static-mutable test injection on a Lazy is fragile

private static Lazy<Boolean> SUPPRESS_CONTENT_URL_FALLBACK = Lazy.of(...);

@VisibleForTesting
public static void setSuppressContentUrlFallback(final boolean value) {
    SUPPRESS_CONTENT_URL_FALLBACK = Lazy.of(() -> value);
}
  • Field is non-volatile mutable static — visibility across threads is not guaranteed. Surefire forks help, but it's still a JVM-wide global.
  • Wrapping a constant in Lazy.of(() -> value) defeats the point of Lazy. A plain static volatile Boolean override (or AtomicReference) would communicate intent better.
  • The setter is public, not package-private, so production code can call it. @VisibleForTesting doesn't enforce anything; consider package-private or moving to a proper feature-flag service.
  • Other static Lazy<Boolean> flags in this codebase (PreventSessionFixationUtil, MetricsStatsJob, etc.) are final — this is a deliberate departure from the established pattern.

A simpler approach: keep the original Lazy as the production read path and use a separate Optional<Boolean> testOverride checked first. Or just inject a Supplier<Boolean> into ContentHelper's constructor.

4. Helper method name is now a lie

private static boolean IsNeitherPageOrFileAsset(Contentlet contentlet) {
    return !contentlet.isFileAsset() && !contentlet.isHTMLPage() && ! contentlet.isDotAsset();
}
  • Name no longer matches behavior — it now also excludes DotAsset. Rename to isRegularContent (or similar) and update the Javadoc, which still reads "neither a file asset nor an HTML page".
  • Java convention is isNeither…, not IsNeither… (capitalized). Pre-existing, but the diff touches the method.
  • Style nit: ! contentlet.isDotAsset() has a stray space between ! and the call (ContentHelper.java:229).

5. @After hard-resets to true — assumes the default

@After
public void resetFlag() {
    ContentHelper.setSuppressContentUrlFallback(true);
}

If anyone changes the default in FeatureFlagName or a test sets up a custom default, this @After will silently drift. Safer: capture the value in @Before, restore in @After, or read Config.getBooleanProperty(...) directly in the reset.

6. Pre-existing bug nearby (FYI, not introduced here)

WebAssetStrategy.urlOverride lines 53-57:

final String url = toolBox.contentHelper.getUrl(contentlet);
if (null != url) {
    map.put(URL_FIELD, url);
}
map.put(URL_FIELD, url);   // unconditional — overwrites with null when getUrl returns null

Not your bug, but the new flag does not affect web assets so this code path is still safe in practice. Worth a separate cleanup PR.

7. Minor — naming of the constant

FEATURE_FLAG_SUPPRESS_CONTENT_URL_FALLBACK — the FEATURE_FLAG_ prefix is redundant given the constant lives in FeatureFlagName. Existing flags in the file are inconsistent (some prefixed, some not), so just calling it out.


Summary

Logic is correct and tests are reasonable. The main risks are (a) the default-on behavior change for the REST contract — verify clients/contracts intentionally — and (b) the mutable-static test seam, which works but should either be tightened (package-private + volatile) or refactored toward DI. The renamed helper method and PR description need cleanup.
• Branch: feat/suppress-content-url-fallback-35583

…L fallback

Returning null lets DefaultTransformStrategy omit the url key entirely from the
REST response map, which is cleaner than url:"" for API consumers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regular contentlets no longer include a synthetic url field after the
suppress-content-url-fallback change (#35583). The assertion was testing
a bug, not a valid API contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…egular contentlets

Regular contentlets no longer carry a URL when SUPPRESS_CONTENT_URL_FALLBACK is
enabled (default: true). The assertNotNull check on the url field is now scoped
to pages and file assets, which always require identifier-based URL resolution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ContentHelper.IsNeitherPageOrFileAsset excludes dotAssets from suppression
(pages, file assets, and dot assets always need an identifier-based URL).
The test guard must mirror that same set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split into Transformer_Simple_Test_SuppressFlagOn and _SuppressFlagOff.
Flag ON (default): web assets carry a URL, regular contentlets must not.
Flag OFF: every contentlet receives a URL (synthetic fallback included).
Each test pins and resets the flag via ContentHelper.setSuppressContentUrlFallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…k as public

The refactoring of Transformer_Simple_Test left a dangling closing brace that
closed the class at line 225, causing 100+ compilation errors in the remaining
test methods. Also widened setSuppressContentUrlFallback visibility from
package-private to public so integration tests in different packages can control
the flag.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ppression

test_find_hydrated: removed containsKey("url") — line already validates hydration
via map size comparison; url is intentionally absent for regular contentlets with
SUPPRESS_CONTENT_URL_FALLBACK enabled.

test_pull_hydrated: replaced containsKey("url") with containsKey("hostName"),
which is always added by DefaultTransformStrategy regardless of content type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Add feature-flagged behavior to skip URL hydration for non-Page/FileAsset/dotAsset contentlets in Content REST API

1 participant