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
- In a dotCMS instance, set the flag in
dotmarketing-config.properties (or via env var):
FEATURE_FLAG_UVE_TOGGLE_LOCK=True
- Restart the server.
- Call
GET /api/v1/configuration/config?keys=FEATURE_FLAG_UVE_TOGGLE_LOCK with admin credentials.
- 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)
Falsy set (case-insensitive, whitespace-trimmed)
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)
NOT_FOUND (flag absent — preserved verbatim)
Regression coverage
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_FOUND → true. |
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_FOUND → true. |
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
Tests
Out of scope
- The
NOT_FOUND → true 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-146 — recoveryFromConfig 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
Problem Statement
Feature flag values defined in
dotmarketing-config.properties(or overridden viaDOT_*environment variables) are coerced to booleans using a strict, case-sensitive comparison. Only the exact lowercase stringtrueresolves totrue; any other casing (True,TRUE) or the numeric truthy value1is treated asfalse.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:
FEATURE_FLAG_UVE_TOGGLE_LOCK=True(capitalized) on their environment.dotCMS/src/main/resources/dotmarketing-config.properties:864ships asFEATURE_FLAG_UVE_TOGGLE_LOCK=false.Trueoverride should have flipped it to enabled, but the case-sensitive coercion silently kept itfalse.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,1as 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 (backendConfigreads and frontendDotPropertiesService.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
dotmarketing-config.properties(or via env var):GET /api/v1/configuration/config?keys=FEATURE_FLAG_UVE_TOGGLE_LOCKwith admin credentials.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 coercionvalue === 'true'evaluatesfalse; the button is hidden.The same reproduction applies for
TRUEand1— both should be treated as truthy but are not.Acceptance Criteria
The parsing must happen on the backend.
/api/v1/configuration/configmust return native JSON booleans (true/false) for every defined feature flag, regardless of the casing or shorthand used indotmarketing-config.propertiesorDOT_*env overrides. The frontend must not have to coerce strings — its only remaining responsibility is theNOT_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 missesTrue,TRUE, and1:After the fix, the same call must return native booleans for any defined flag, with
NOT_FOUNDas the only string sentinel:Resolution must be consistent and applied wherever feature flag boolean coercion happens — primarily
ConfigurationResource(and the underlyingConfighelpers it calls). The frontend coercion inDotPropertiesServiceshould become a no-op for boolean values arriving from the API; only theNOT_FOUNDsentinel needs handling.Truthy set (case-insensitive, whitespace-trimmed)
true,True,TRUE,true→ resolve astrue1→ resolves astrueFalsy set (case-insensitive, whitespace-trimmed)
false,False,FALSE,false→ resolve asfalse0→ resolves asfalsenull→ resolve asfalse(explicitly defined; flag absent /NOT_FOUNDkeeps the existing "default true" semantics — see "Out of scope" below)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".FEATURE_FLAG_UVE_TOGGLE_LOCK.DotPropertiesService.getFeatureFlags/getFeatureFlag) no longer needs to coerce string-to-boolean for feature flag values; it only needs to apply theNOT_FOUND→ enabled default. Existing string-coercion logic should be removed or simplified once the backend contract is in place.enabled,maybe) is logged asWARNonce at startup or first read so misconfigurations are visible to operators, and resolves asfalsein the API response (current safe-default behavior preserved).NOT_FOUND(flag absent — preserved verbatim).propertiesentry, noDOT_*env override, no dynamic property), the API response must continue to return the literal string"NOT_FOUND"for that key — exactly as today.FEATURE_FLAG_UVE_STYLE_EDITORis not set:{ "FEATURE_FLAG_UVE_STYLE_EDITOR": "NOT_FOUND" }NOT_FOUNDsentinel is not coerced totrueorfalseon the backend — it is returned as-is so the frontend can apply its existing opt-out default (NOT_FOUND→ enabled).ConfigurationResourceto return native booleans for feature flag keys, theNOT_FOUNDcase must be the explicit exception: still emitted as the string"NOT_FOUND", never as a boolean.Regression coverage
FEATURE_FLAG_UVE_TOGGLE_LOCK=True) → UVE lock/unlock button renders.true/falsecontinue 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/configfrom 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.core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.ts:88value === 'true'NOT_FOUND→true.core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.ts:117value === 'true' ? true : value === 'false' ? false : valueNOT_FOUND→true.core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/flags/withFlags.ts:39value === true || value === FEATURE_FLAG_NOT_FOUNDcore-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.ts:106FEATURE_FLAG_PAGE_SCANNER=== truecore-web/libs/portlets/edit-ema/portlet/src/lib/store/features/workflow/withWorkflow.ts:121FEATURE_FLAG_UVE_TOGGLE_LOCKfalse.core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts:140FEATURE_FLAG_UVE_STYLE_EDITORcore-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts:148FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION=== truecore-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts:61FEATURE_FLAG_CONTENT_EDITOR2_ENABLED=== 'true'(string)core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/service/field-properties.service.ts:48FEATURE_FLAG_CONTENT_EDITOR2_RENDER_MODE_DEFAULTIFRAME/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:79FEATURE_FLAG_UVE_STYLE_EDITORgetFeatureFlag(boolean)core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit.routes.ts:17FEATURE_FLAG_CONTENT_EDITOR2_ENABLEDcore-web/apps/dotcms-ui/src/app/api/services/guards/edit-content.guard.ts:13FEATURE_FLAG_CONTENT_EDITOR2_ENABLEDgetFeatureFlag(boolean)core-web/apps/dotcms-ui/src/app/api/services/guards/pages-guard.service.ts:22DOTFAVORITEPAGE_FEATURE_ENABLEgetFeatureFlag(boolean)core-web/apps/dotcms-ui/src/app/view/components/dot-toolbar/dot-toolbar.component.ts:47FEATURE_FLAG_ANNOUNCEMENTSVerification checklist
dot-custom-event-handler.service.ts:61is updated to handle native boolean (string check removed).FEATURE_FLAG_CONTENT_EDITOR2_RENDER_MODE_DEFAULTis confirmed not affected (still a string-typed config value).=== 'true'or=== 'false'string check is left on a feature flag value acrosscore-web/libsandcore-web/apps.getFeatureFlag()and batchgetFeatureFlags()inDotPropertiesServiceare updated so native booleans pass through unchanged and theNOT_FOUNDsentinel still resolves to enabled.Tests
/api/v1/configuration/configreturns:truefor at least one flag set with each of: lowercasetrue, capitalizedTrue,TRUE,1falsefor at least one flag set with each of:false,False,FALSE,0"NOT_FOUND"for at least one flag that is not defined anywheredot-properties.service.spec.tsis updated to reflect the new contract: native booleans pass through unchanged,"NOT_FOUND"is mapped to enabled.Out of scope
NOT_FOUND→trueopt-out default behavior is intentional and preserved.dotmarketing-config.properties; it only fixes how values are parsed.Config.getBooleanPropertyconsumers unrelated to flags) is left to the implementer's judgment — if the cleanest fix is at theConfiglayer, 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
mainbranch — 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— defaultFEATURE_FLAG_UVE_TOGGLE_LOCK=falsedotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java:134-146—recoveryFromConfigreturns raw string for non-prefixed keyscore-web/libs/data-access/src/lib/dot-properties/dot-properties.service.ts:107-126— strictvalue === 'true'/value === 'false'coercioncore-web/libs/portlets/edit-ema/portlet/src/lib/store/features/flags/withFlags.ts:35-42— secondary normalization (only checks booleantrueandNOT_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