Workflow fire silently discards host/contentHost/hostFolder when the content type has no Site-or-Folder field
Nobody has claimed this yet.
- 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:
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 host → contentHost to avoid colliding with the Contentlet's own host property.
Two secondary silent-failure layers compound this:
processHostOrFolderFieldswallows every exception —catch (Exception ex) { // just pass }(L587-L589). Even when the field does exist, an unresolvable site produces no error.ContentTypeAPIsave:SiteAndFolderResolverImpl.resolveSitecheckssiteNamebeforehostand letssiteNamewin (L113-L119). APUTbuilt from aGETbody carriessiteName, which silently overrides an explicitly-sethostand 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
-
Create a content type with no Site-or-Folder field, and no
hostin the POST body:{ "clazz": "CONTENT", "name": "Book", "variable": "Book", "fields": [ ... ] }Observe it is assigned to the default site (via
SiteAndFolderResolverImpl.fallbackHost()). -
Create a second site, e.g.
awazon.local. -
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": "...", ... } } -
Actual: HTTP 200,
errors: [],live: true. Re-fetch the contentlet —hostis the default site id,hostNameisdefault. The submittedhostwas discarded with no error, no warning, no log entry.
Expected: either the host is honored, or the response tells the caller it was not. -
Repeat step 3 substituting
contentHost,hostFolder,folder, orhost+foldertogether — all silently discarded, all HTTP 200. -
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
WARNlog, preferably an advisory message on the response envelope (theMessageEntitymechanism already used for Story Block conversion warnings is a good precedent). - Specifically, when
host,contentHost,hostFolder, orfolderis 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. -
processHostOrFolderFieldno 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
hostandsiteNameare provided and resolve to different sites, the conflict is surfaced rather than silently resolved in favor ofsiteName. - Documentation / OpenAPI descriptions for the fire endpoints state plainly that
contentHostis 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
hoston a type without a Site-or-Folder field (expect warning surfaced); fire withcontentHoston a type with one (expect honored); content-typePUTwith conflictinghost/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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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