Skip to content

Feature flag boolean parsing is case-sensitive — accept industry-standard truthy/falsy values #35551

@oidacra

Description

@oidacra

Problem Statement

Feature flag values defined in dotmarketing-config.properties (or overridden via DOT_* environment variables) are coerced to booleans using a strict, case-sensitive comparison. Only the exact lowercase string true resolves to true; any other casing (True, TRUE) or the numeric truthy value 1 is treated as false.

This was surfaced by a customer support ticket where the lock/unlock button in the Universal Visual Editor (UVE) disappeared after a maintenance window. Investigation showed:

  • Customer had FEATURE_FLAG_UVE_TOGGLE_LOCK=True (capitalized) on their environment.
  • The default in dotCMS/src/main/resources/dotmarketing-config.properties:864 ships as FEATURE_FLAG_UVE_TOGGLE_LOCK=false.
  • The customer's True override should have flipped it to enabled, but the case-sensitive coercion silently kept it false.
  • The UVE store gates the lock button on store.flags().FEATURE_FLAG_UVE_TOGGLE_LOCK — the button never rendered.

This is a regression in user expectation: every other system in the dotCMS ecosystem (Docker env vars, Spring configuration, shell scripts) treats True, TRUE, 1 as truthy. Customers and operators should not have to know an undocumented internal convention to make a flag work.

The bug is not specific to FEATURE_FLAG_UVE_TOGGLE_LOCK — it affects every boolean feature flag and every consumer of the flag-resolution path (backend Config reads and frontend DotPropertiesService.getFeatureFlags).

Impact: customer-visible feature regressions any time a flag value is set with non-canonical casing or with a numeric/boolean-shorthand truthy value. Silent (no log warning), hard to diagnose without reading source.

Steps to Reproduce

  1. In a dotCMS instance, set the flag in dotmarketing-config.properties (or via env var):
    FEATURE_FLAG_UVE_TOGGLE_LOCK=True
    
  2. Restart the server.
  3. Call GET /api/v1/configuration/config?keys=FEATURE_FLAG_UVE_TOGGLE_LOCK with admin credentials.
  4. Open the UVE on any page in EDIT mode.

Expected: API returns a truthy value for the flag; the lock/unlock button renders in the UVE toolbar.

Actual: API returns the raw string "True"; the frontend coercion value === 'true' evaluates false; the button is hidden.

The same reproduction applies for TRUE and 1 — both should be treated as truthy but are not.

Acceptance Criteria

The parsing must happen on the backend. /api/v1/configuration/config must return native JSON booleans (true / false) for every defined feature flag, regardless of the casing or shorthand used in dotmarketing-config.properties or DOT_* env overrides. The frontend must not have to coerce strings — its only remaining responsibility is the NOT_FOUND → enabled opt-out default.

Current (broken) vs Expected response

Today /api/v1/configuration/config?keys=... returns raw strings, forcing the frontend to coerce them with a strict, case-sensitive check that misses True, TRUE, and 1:

// CURRENT
{
  "FEATURE_FLAG_PAGE_SCANNER": "false",
  "FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION": "false",
  "FEATURE_FLAG_UVE_STYLE_EDITOR": "NOT_FOUND",
  "FEATURE_FLAG_UVE_TOGGLE_LOCK": "True"
}

After the fix, the same call must return native booleans for any defined flag, with NOT_FOUND as the only string sentinel:

// EXPECTED
{
  "FEATURE_FLAG_PAGE_SCANNER": false,
  "FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION": false,
  "FEATURE_FLAG_UVE_STYLE_EDITOR": "NOT_FOUND",
  "FEATURE_FLAG_UVE_TOGGLE_LOCK": true
}

Resolution must be consistent and applied wherever feature flag boolean coercion happens — primarily ConfigurationResource (and the underlying Config helpers it calls). The frontend coercion in DotPropertiesService should become a no-op for boolean values arriving from the API; only the NOT_FOUND sentinel needs handling.

Truthy set (case-insensitive, whitespace-trimmed)

  • true, True, TRUE, true → resolve as true
  • 1 → resolves as true

Falsy set (case-insensitive, whitespace-trimmed)

  • false, False, FALSE, false → resolve as false
  • 0 → resolves as false
  • empty string and null → resolve as false (explicitly defined; flag absent / NOT_FOUND keeps the existing "default true" semantics — see "Out of scope" below)

Out of scope for this issue: yes/no, on/off, single-letter y/n. Only the canonical booleans (true/false, any case) and numeric 1/0 are accepted.

API contract (where the parsing happens)

  • ConfigurationResource (and any other endpoint that exposes feature flags) returns native JSON booleans for every defined feature flag — never strings like "true" / "True" / "false".
  • The same parsing rules apply to every feature flag — not just FEATURE_FLAG_UVE_TOGGLE_LOCK.
  • The frontend (DotPropertiesService.getFeatureFlags / getFeatureFlag) no longer needs to coerce string-to-boolean for feature flag values; it only needs to apply the NOT_FOUND → enabled default. Existing string-coercion logic should be removed or simplified once the backend contract is in place.
  • An invalid / unrecognized string value (e.g. enabled, maybe) is logged as WARN once at startup or first read so misconfigurations are visible to operators, and resolves as false in the API response (current safe-default behavior preserved).

NOT_FOUND (flag absent — preserved verbatim)

  • When a feature flag property is not defined anywhere (no .properties entry, no DOT_* env override, no dynamic property), the API response must continue to return the literal string "NOT_FOUND" for that key — exactly as today.
    • Example response when FEATURE_FLAG_UVE_STYLE_EDITOR is not set:
      { "FEATURE_FLAG_UVE_STYLE_EDITOR": "NOT_FOUND" }
  • The NOT_FOUND sentinel is not coerced to true or false on the backend — it is returned as-is so the frontend can apply its existing opt-out default (NOT_FOUND → enabled).
  • If the implementation switches ConfigurationResource to return native booleans for feature flag keys, the NOT_FOUND case must be the explicit exception: still emitted as the string "NOT_FOUND", never as a boolean.

Regression coverage

  • Customer reproduction (FEATURE_FLAG_UVE_TOGGLE_LOCK=True) → UVE lock/unlock button renders.
  • Existing flags currently set to lowercase true/false continue to work unchanged.
  • NOT_FOUND (flag absent on server) continues to be treated as enabled by default — this PR does not change opt-out semantics.

Frontend consumer audit (must verify each call site after BE change)

Switching /api/v1/configuration/config from raw strings to native JSON booleans is a contract change. Every frontend consumer that reads a feature flag must be inspected — string-equality checks (=== 'true', === 'false') will silently break, and ambiguous truthy checks may behave differently. For each entry below, verify the gated UI still renders/behaves correctly and update the check style if needed.

Call site Flag Current check Action
core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.ts:88 (any, single) value === 'true' Update so native booleans pass through; keep NOT_FOUNDtrue.
core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.ts:117 (any, batch) value === 'true' ? true : value === 'false' ? false : value Update so native booleans pass through unchanged; keep NOT_FOUNDtrue.
core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/flags/withFlags.ts:39 UVE flags value === true || value === FEATURE_FLAG_NOT_FOUND Already strict-boolean; verify unchanged behavior.
core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.ts:106 FEATURE_FLAG_PAGE_SCANNER === true Verify Page Scanner UI renders when flag is on.
core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/workflow/withWorkflow.ts:121 FEATURE_FLAG_UVE_TOGGLE_LOCK truthy (no operator) Verify UVE lock/unlock button renders for the customer repro and stays hidden when flag is false.
core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts:140 FEATURE_FLAG_UVE_STYLE_EDITOR truthy (no operator) Verify Style Editor entry point in UVE.
core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts:148 FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION === true Verify legacy script injection toggle behaves correctly.
core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts:61 FEATURE_FLAG_CONTENT_EDITOR2_ENABLED === 'true' (string) ⚠️ Will break when API returns native boolean. Must be updated to handle native boolean. Verify "edit-contentlet" custom event routes to the new editor when enabled.
core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/service/field-properties.service.ts:48 FEATURE_FLAG_CONTENT_EDITOR2_RENDER_MODE_DEFAULT string-typed (NOT a boolean flag) Confirm this key is excluded from the boolean-coercion path on the backend (it stores a render mode string like IFRAME/INLINE, not a boolean). Behavior must remain unchanged.
core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/layout/content-types-layout.component.ts:79 FEATURE_FLAG_UVE_STYLE_EDITOR getFeatureFlag (boolean) Verify Style Editor tab visibility.
core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit.routes.ts:17 FEATURE_FLAG_CONTENT_EDITOR2_ENABLED resolver-based Verify route guard / resolver still enables the new editor route.
core-web/apps/dotcms-ui/src/app/api/services/guards/edit-content.guard.ts:13 FEATURE_FLAG_CONTENT_EDITOR2_ENABLED getFeatureFlag (boolean) Verify guard redirects correctly when flag is on/off.
core-web/apps/dotcms-ui/src/app/api/services/guards/pages-guard.service.ts:22 DOTFAVORITEPAGE_FEATURE_ENABLE getFeatureFlag (boolean) Verify favorite-page guard.
core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts:47 FEATURE_FLAG_ANNOUNCEMENTS reference, consumed downstream Verify Announcements widget visibility.

Distinct read path — contentlet metadata, not Config: FEATURE_FLAG_CONTENT_EDITOR2_ENABLED is also read from contentType.metadata in several places (block-editor, edit-content, dot-content-drive). That value comes from the content type's stored metadata map, NOT from /api/v1/configuration/config. It is unaffected by this change but must not be confused with the Config flag during the audit.

Verification checklist

  • Every call site listed above is exercised manually or by automated test with the flag both on and off; expected UI renders in each state.
  • dot-custom-event-handler.service.ts:61 is updated to handle native boolean (string check removed).
  • FEATURE_FLAG_CONTENT_EDITOR2_RENDER_MODE_DEFAULT is confirmed not affected (still a string-typed config value).
  • No remaining === 'true' or === 'false' string check is left on a feature flag value across core-web/libs and core-web/apps.
  • Single-flag getFeatureFlag() and batch getFeatureFlags() in DotPropertiesService are updated so native booleans pass through unchanged and the NOT_FOUND sentinel still resolves to enabled.
  • Existing unit tests in the call-site spec files pass with mocks updated to emit native booleans (matching the new BE contract).

Tests

  • Unit tests on the backend cover every truthy and falsy variant listed above and assert that the resulting REST response field is a native JSON boolean (not a string).
  • Integration test (or postman collection update) confirms /api/v1/configuration/config returns:
    • native true for at least one flag set with each of: lowercase true, capitalized True, TRUE, 1
    • native false for at least one flag set with each of: false, False, FALSE, 0
    • the literal string "NOT_FOUND" for at least one flag that is not defined anywhere
  • Frontend unit test in dot-properties.service.spec.ts is updated to reflect the new contract: native booleans pass through unchanged, "NOT_FOUND" is mapped to enabled.

Out of scope

  • The NOT_FOUNDtrue opt-out default behavior is intentional and preserved.
  • This issue does not modify the default value of any specific flag in dotmarketing-config.properties; it only fixes how values are parsed.
  • Boolean coercion outside the feature-flag path (e.g., generic Config.getBooleanProperty consumers unrelated to flags) is left to the implementer's judgment — if the cleanest fix is at the Config layer, the broader benefit is acceptable; if scoped to the flag pipeline, that is also acceptable, as long as the AC above are met uniformly.

dotCMS Version

Latest from main branch — also reproduces in the customer's environment running the previous LTS line. Regression appears to be pre-existing rather than introduced in a recent release, but became visible to the customer after a recent maintenance window where their flag configuration was reapplied.

Severity

Medium - Some functionality impacted

Links

Relevant code references

  • dotCMS/src/main/resources/dotmarketing-config.properties:864 — default FEATURE_FLAG_UVE_TOGGLE_LOCK=false
  • dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java:134-146recoveryFromConfig returns raw string for non-prefixed keys
  • core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.ts:107-126 — strict value === 'true' / value === 'false' coercion
  • core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/flags/withFlags.ts:35-42 — secondary normalization (only checks boolean true and NOT_FOUND)
  • core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/workflow/withWorkflow.ts:121,147 — consumer that hides the UVE lock button when the flag resolves false

Metadata

Metadata

Assignees

Type

No fields configured for Bug.

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions