Skip to content

Flaky Postman tests: WorkflowResource silently defaults missing languageId to -1L causing FK violation #35780

@dsolistorres

Description

@dsolistorres

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_collectionNot anonymous layout / PUT Create Page Using NOT anonymous Template — HTTP 500 from PUT /api/v1/workflow/actions/default/fire/NEW
  • AI.postman_collectionSearch / 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

  1. Open any PR that runs the PR Test / Postman Tests - Template job
  2. Observe intermittent failure on Not anonymous layout / PUT Create Page Using NOT anonymous Template
  3. 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

  • WorkflowResource.fireActionDefaultSinglePart (and the four sibling sites with the -1l literal) no longer silently default missing languageId to -1L. Either a valid default language id is resolved, or the request returns 400 with a clear error message.
  • Existing Postman tests for Template_Resource and AI collections pass deterministically across 20+ consecutive PR builds (sample via a tracking branch + re-runs).
  • Add unit test for WorkflowResource covering the missing-languageId case: asserts 400 (not 500, not silent -1 INSERT).
  • No regression in existing Postman tests that do pass languageId correctly.

dotCMS Version

Evergreen 26.05.11-01

Severity

Medium — intermittent CI failure blocking PR merges; affects multiple branches; no customer-facing impact.

Links

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    Status

    New

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions