Problem Statement
Multiple Postman test collections (Template_Resource, AI) intermittently fail in PR check runs with the same server-side signature:
ERROR: insert or update on table "contentlet" violates foreign key constraint "fk_contentlet_lang"
Detail: Key (language_id)=(-1) is not present in table "language".
Affected tests observed in run 26167537035 (PR #35771):
Template_Resource.postman_collection → Not anonymous layout / PUT Create Page Using NOT anonymous Template — HTTP 500 from PUT /api/v1/workflow/actions/default/fire/NEW
AI.postman_collection → Search / Search Related by identifier — HTTP 404 from POST /api/v1/ai/search/related
Impact: blocks PR merges; affects multiple unrelated PRs over the last week; not customer-facing.
Root Cause
The endpoint silently defaults a missing languageId to -1L and then attempts a DB INSERT with it.
dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java:3476:
final long languageId = ConversionUtils.toLong(contentMap.get("languageId"), -1l);
When the Postman request body omits languageId (or its pre-request script's lookup of the default language returns nothing — e.g., cold cache), the request progresses with languageId = -1L. The call chain:
WorkflowResource.fireActionDefaultSinglePart → fireAction
→ WorkflowAPIImpl.fireContentWorkflow
→ SaveContentAsDraftActionlet.executeAction
→ ESContentletAPIImpl.checkin → internalCheckin
→ ESContentFactoryImpl.upsertContentlet
→ PostgreUpsertCommand.execute
→ Postgres rejects with fk_contentlet_lang violation
A second site of the same pattern exists in com.liferay.portal.language.LanguageUtil.internalGetLanguageId (also returns -1 on missing input). Both sites have existed for years — the intermittency is what changed, likely from recent timing/load shifts (large OpenSearch refactor work over the past two weeks). The defaulting itself is the structural defect.
Steps to Reproduce
- Open any PR that runs the
PR Test / Postman Tests - Template job
- Observe intermittent failure on
Not anonymous layout / PUT Create Page Using NOT anonymous Template
- Inspect dotCMS server log around the failing request — confirm
language_id)=(-1) FK violation
Or, deterministically reproduce locally:
curl -X PUT -u admin@dotcms.com:admin \
-H "Content-Type: application/json" \
-d '{"contentlet": {"contentType": "htmlpageasset", "title": "test"}}' \
http://localhost:8080/api/v1/workflow/actions/default/fire/NEW
Returns 500 with the FK violation in the server log.
Suggested Fixes
Three options, in order of preference:
Option A (preferred) — Server-side: fail fast with 400, never INSERT -1
Replace the silent -1L default with explicit validation in WorkflowResource.fireActionDefaultSinglePart (and the sibling sites at lines 2645, 2821, 3182, 3725):
// Before:
final long languageId = ConversionUtils.toLong(contentMap.get("languageId"), -1l);
// After:
final long languageId = ConversionUtils.toLong(contentMap.get("languageId"),
APILocator.getLanguageAPI().getDefaultLanguage().getId());
if (languageId <= 0) {
throw new BadRequestException(
"Missing or invalid 'languageId' on contentlet payload");
}
Why preferred: fixes the root cause structurally — clients that forget languageId get a clear 400 instead of a confusing 500, and the FK violation becomes structurally impossible. Affects no real-world client behavior since legitimate requests always include languageId.
Option B — Postman fixture hardening
Add a collection-level pre-request script that resolves the default language ID once per run and stores it as a collection variable:
// Collection-level pre-request script (idempotent via guard)
if (!pm.collectionVariables.get('defaultLanguageId')) {
pm.sendRequest({
url: pm.environment.get('serverURL') + '/api/v1/languages',
method: 'GET',
header: { 'Authorization': pm.environment.get('basicAuth') }
}, (err, res) => {
if (err || res.code !== 200) return;
const langs = res.json();
const def = langs.find(l => l.defaultLanguage === true) || langs[0];
if (def && def.id > 0) pm.collectionVariables.set('defaultLanguageId', def.id);
});
}
Then every request that creates a contentlet uses {{defaultLanguageId}} instead of relying on a server-side default. Trade-off: fixes the symptom in Postman only; the same trap remains for real REST clients.
Option C — Pre-run readiness gate
Add a startup-poll step to the Postman runner (run-postman-tests.sh or the surefire/failsafe wrapper) that hits GET /api/v1/languages until it returns a non-empty list with a valid default before any collection runs. Trade-off: simplest mechanically, but doesn't fix the underlying defect — a real client that calls the endpoint without languageId will still get a confusing 500.
Recommendation: A + B. A removes the structural defect; B makes the tests self-healing against future server-side issues by being explicit.
Acceptance Criteria
dotCMS Version
Evergreen 26.05.11-01
Severity
Medium — intermittent CI failure blocking PR merges; affects multiple branches; no customer-facing impact.
Links
Problem Statement
Multiple Postman test collections (
Template_Resource,AI) intermittently fail in PR check runs with the same server-side signature:Affected tests observed in run 26167537035 (PR #35771):
Template_Resource.postman_collection→Not anonymous layout / PUT Create Page Using NOT anonymous Template— HTTP 500 fromPUT /api/v1/workflow/actions/default/fire/NEWAI.postman_collection→Search / Search Related by identifier— HTTP 404 fromPOST /api/v1/ai/search/relatedImpact: blocks PR merges; affects multiple unrelated PRs over the last week; not customer-facing.
Root Cause
The endpoint silently defaults a missing
languageIdto-1Land then attempts a DB INSERT with it.dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java:3476:When the Postman request body omits
languageId(or its pre-request script's lookup of the default language returns nothing — e.g., cold cache), the request progresses withlanguageId = -1L. The call chain:A second site of the same pattern exists in
com.liferay.portal.language.LanguageUtil.internalGetLanguageId(also returns-1on missing input). Both sites have existed for years — the intermittency is what changed, likely from recent timing/load shifts (large OpenSearch refactor work over the past two weeks). The defaulting itself is the structural defect.Steps to Reproduce
PR Test / Postman Tests - TemplatejobNot anonymous layout / PUT Create Page Using NOT anonymous Templatelanguage_id)=(-1)FK violationOr, deterministically reproduce locally:
Returns 500 with the FK violation in the server log.
Suggested Fixes
Three options, in order of preference:
Option A (preferred) — Server-side: fail fast with 400, never INSERT -1
Replace the silent
-1Ldefault with explicit validation inWorkflowResource.fireActionDefaultSinglePart(and the sibling sites at lines 2645, 2821, 3182, 3725):Why preferred: fixes the root cause structurally — clients that forget
languageIdget a clear 400 instead of a confusing 500, and the FK violation becomes structurally impossible. Affects no real-world client behavior since legitimate requests always includelanguageId.Option B — Postman fixture hardening
Add a collection-level pre-request script that resolves the default language ID once per run and stores it as a collection variable:
Then every request that creates a contentlet uses
{{defaultLanguageId}}instead of relying on a server-side default. Trade-off: fixes the symptom in Postman only; the same trap remains for real REST clients.Option C — Pre-run readiness gate
Add a startup-poll step to the Postman runner (
run-postman-tests.shor the surefire/failsafe wrapper) that hitsGET /api/v1/languagesuntil it returns a non-empty list with a valid default before any collection runs. Trade-off: simplest mechanically, but doesn't fix the underlying defect — a real client that calls the endpoint withoutlanguageIdwill still get a confusing 500.Recommendation: A + B. A removes the structural defect; B makes the tests self-healing against future server-side issues by being explicit.
Acceptance Criteria
WorkflowResource.fireActionDefaultSinglePart(and the four sibling sites with the-1lliteral) no longer silently default missinglanguageIdto-1L. Either a valid default language id is resolved, or the request returns 400 with a clear error message.Template_ResourceandAIcollections pass deterministically across 20+ consecutive PR builds (sample via a tracking branch + re-runs).WorkflowResourcecovering the missing-languageIdcase: asserts 400 (not 500, not silent -1 INSERT).languageIdcorrectly.dotCMS Version
Evergreen 26.05.11-01
Severity
Medium — intermittent CI failure blocking PR merges; affects multiple branches; no customer-facing impact.
Links