dotCMS / dotCMS/core

Workflow fire silently discards host/contentHost/hostFolder when the content type has no Site-or-Folder field

Open
#37,040 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Priority : 3 Average Team : Scout Type : Defect
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

Problem Statement

When firing a workflow action to create a contentlet, any key in the contentlet body that does not match a field on the content type is silently discarded. The response still returns HTTP 200 with errors: [], a real identifier, and live: true — so the caller has no signal that part of their payload was ignored.

This is most damaging for site placement. Callers reasonably send host, contentHost, hostFolder, or folder to put content on a specific site. For a content type without a Site-or-Folder field, none of these do anything. The content silently lands on the content type's own host (usually the default site), and the response echoes back a host that differs from what was sent, with no error and no log line.

Why this is hard to diagnose

The failure is invisible until something host-scoped breaks much later. In the case that surfaced this, 18 contentlets were created on the wrong site. Home and listing pages rendered perfectly (container queries aren't host-scoped), so the only symptom was a 404 on the urlmap detail page — urlmap resolution is per-host. The stored contentlet even showed a correctly computed URL_MAP_FOR_CONTENT; it was just on the wrong site.

Diagnosing it took eight failed attempts (hostFolder, host+hostFolder, hostFolder: '//site/', folder: <id>, content-type PUT, Host: request header, contentHost, edit-in-place), each returning 200 with errors: [].

Root cause

MapToContentletPopulator.fillFields only processes keys present in the content type's field map:

https://github.com/dotCMS/core/blob/main/dotCMS/src/main/java/com/dotcms/rest/MapToContentletPopulator.java#L247-L283

final Field field = fieldMap.get(key);
if (field != null) {
    if (field.getFieldType().equals(FieldType.HOST_OR_FOLDER.toString())) {
        this.processHostOrFolderField(contentlet, type, value);   // only site-setting path
    } ...
}
// no else branch — unknown keys fall through with no error and no log

So a body key sets the site only if the content type has a HostFolderField whose variable equals that key. The key name is irrelevant — the field is what matters. contentHost is not a reserved key; it is the conventional variable name that OOTB webPageContent uses, which is why every Postman collection that sends contentHost also targets webPageContent. Its origin is FixTask00011RenameHostInFieldVariableName, which renames any Site-or-Folder field variable hostcontentHost to avoid colliding with the Contentlet's own host property.

Two secondary silent-failure layers compound this:

  1. processHostOrFolderField swallows every exception — catch (Exception ex) { // just pass } (L587-L589). Even when the field does exist, an unresolvable site produces no error.
  2. ContentTypeAPI save: SiteAndFolderResolverImpl.resolveSite checks siteName before host and lets siteName win (L113-L119). A PUT built from a GET body carries siteName, which silently overrides an explicitly-set host and resolves the type right back to where it was. Returns 200, no error.

The eventual fallback chain that decides placement is ESContentletAPIImpl.populateHost() (L10759-L10784): explicit host/folder → sibling in any language → content type's host/folder → SYSTEM_HOST.

Steps to Reproduce

  1. Create a content type with no Site-or-Folder field, and no host in the POST body:

    { "clazz": "CONTENT", "name": "Book", "variable": "Book", "fields": [ ... ] }
    

    Observe it is assigned to the default site (via SiteAndFolderResolverImpl.fallbackHost()).

  2. Create a second site, e.g. awazon.local.

  3. Fire a create on that type, explicitly passing a host:

    PUT /api/v1/workflow/actions/default/fire/PUBLISH?indexPolicy=WAIT_FOR
    { "contentlet": { "contentType": "Book", "host": "<awazon.local site id>", "title": "...", ... } }
    
  4. Actual: HTTP 200, errors: [], live: true. Re-fetch the contentlet — host is the default site id, hostName is default. The submitted host was discarded with no error, no warning, no log entry.
    Expected: either the host is honored, or the response tells the caller it was not.

  5. Repeat step 3 substituting contentHost, hostFolder, folder, or host+folder together — all silently discarded, all HTTP 200.

  6. If the type has a urlmap, the mis-placed content 404s on its detail URL while listing pages render fine, because urlmap resolution is per-host and container queries are not.

Acceptance Criteria

  • Body keys in a workflow fire request that match no field on the target content type are surfaced to the caller — at minimum a WARN log, preferably an advisory message on the response envelope (the MessageEntity mechanism already used for Story Block conversion warnings is a good precedent).
  • Specifically, when host, contentHost, hostFolder, or folder is sent to a content type that has no Site-or-Folder field, the caller receives a clear message explaining that site placement requires such a field (or the content type's own host), rather than a silent 200.
  • processHostOrFolderField no longer swallows all exceptions silently — a value that cannot be resolved to a site or folder is logged and reported, not discarded.
  • Content type save: when both host and siteName are provided and resolve to different sites, the conflict is surfaced rather than silently resolved in favor of siteName.
  • Documentation / OpenAPI descriptions for the fire endpoints state plainly that contentHost is a field variable name, not an endpoint parameter, and that it only works on content types that carry a Site-or-Folder field.
  • Test coverage for: fire with host on a type without a Site-or-Folder field (expect warning surfaced); fire with contentHost on a type with one (expect honored); content-type PUT with conflicting host/siteName.

dotCMS Version

Latest from main (verified at commit 4d49cfec4a).

Severity

Medium - Some functionality impacted

The API is behaving as designed once you know the design; the defect is that the design is undiscoverable and fails silently, producing wrong data that surfaces far from its cause.

Links

NA

Notes

There is a non-destructive recovery path for content already placed on the wrong site — the workflow Move action, which relocates a contentlet while preserving its identifier and live status:

PUT /api/v1/workflow/actions/<moveActionId>/fire?identifier=<id>&indexPolicy=WAIT_FOR
{ "pathToMove": "//awazon.local/" }

This works (Contentlet.PATH_TO_MOVE, WorkflowResource.java:5262) and is worth documenting alongside the fix, since recreating content is often blocked by unique-field constraints on urlTitle.

The forward-looking fix for callers is to add a Site-or-Folder field to the content type:

{ "clazz": "com.dotcms.contenttype.model.field.ImmutableHostFolderField",
  "name": "Site", "variable": "contentHost", "required": true, "indexed": true }

...which makes contentHost in the fire body work per-contentlet, and prevents an edit from resetting the contentlet to the type's host.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with MapToContentletPopulator.fillFields and processHostOrFolderField, then read SiteAndFolderResolverImpl.resolveSite and the workflow fire response handling. Inspect existing MessageEntity and workflow/API tests before covering unknown keys, unresolved sites, and host/siteName conflicts. Done means warnings or conflicts are surfaced, documentation is updated, and the specified fire and content-type tests pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
api, backend, documentation
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.