Part of EPIC #698.
Repro:
curl -X POST "http://localhost:8080/sites/<site>/home.jsAction.do?name=<module>%2F<action>" \
-H "X-JS-Action: 1" -H "Content-Type: text/plain;charset=UTF-8" \
--data-binary '<devalue payload>'
# → HTTP 200, Content-Length: 0
The action executes (side effects and console.info happen), GenericActionEndpoint produces its JSON envelope, but without accept: application/json the response body is dropped somewhere in the render/action chain — empty 200, nothing logged. Every error path (Unknown action, missing header, validation failure) is equally invisible without the header.
The shipped client stub always sends accept: application/json, so browser calls are unaffected — but any hand-rolled caller (curl, server-to-server integration, tests) gets silent empty responses that are painful to diagnose (this evaluation initially misread it as an endpoint outage).
Expected: the endpoint should return its JSON envelope regardless of the accept header (it has no other representation), or respond 406 with an explanatory body — anything but empty 200.
Technical plan (agent-executable)
Written for execution by a Claude (Opus/Sonnet) agent inside the Jahia Cortex harness. ⚠️ GenericActionEndpoint exists only on the js-server-extensions feature branch (ADR-0008) — target that branch or wherever it has landed.
Harness setup
- Open Cortex;
cortex-sources → javascript-modules @ the feature branch. For the investigation step, also bring in Jahia core sources (jahia-private) read-only via cortex-sources.
- Running Jahia via
jahia-docker; build engine mvn install -pl javascript-modules-engine -am -DskipTests; deploy via jahia-deploy; deploy any module exposing a .action.ts (e.g. the test module) with its repo-native yarn deploy.
Reproduction (baseline — record before/after)
# WITHOUT accept header → today: HTTP 200, Content-Length: 0, nothing logged
curl -si -X POST "http://localhost:8080/sites/<site>/home.jsAction.do?name=<module>%2F<action>" \
-H "X-JS-Action: 1" -H "Content-Type: text/plain;charset=UTF-8" --data-binary '[[1]]'
# WITH the header → JSON envelope ({"data": ...} or {"error": ...})
curl -si ... -H "accept: application/json" ...
Also repro the error paths without the header: unknown action name, missing X-JS-Action — all currently empty 200s.
Step 1 — Confirm the drop site (investigation, ~30 min)
The endpoint (javascript-modules-engine-java/.../actions/GenericActionEndpoint.java) always returns an ActionResult with a JSON body. The body is dropped downstream in Jahia core's action handling — read org.jahia.bin.Render#doAction (and the ActionResult → response serialization it performs) in the core sources to confirm the exact condition on the accept header / jsonResponse behavior. Document the finding in the PR description (one paragraph, with the core file/line). Do not patch core — the fix below is module-side, so it ships with the engine.
Step 2 — Fix (module-side, in GenericActionEndpoint.doExecute)
Write the envelope directly instead of delegating serialization to core:
- Build the JSON envelope exactly as today (
{"data": ...} / {"error": ..., "issues": ...}).
response.setStatus(...), response.setContentType("application/json;charset=UTF-8"), write via response.getWriter(), flush.
- Return the
ActionResult variant that makes core write nothing further — verify against core's doAction handling of (a) null result, (b) a result with a null JSON payload; pick the one that neither 404s nor double-writes, and leave a comment citing the core behavior. If neither is clean, keep returning the JSON ActionResult when accept contains application/json (byte-identical to today for the stub) and direct-write only otherwise.
- Status codes while here: validation errors → 400, unknown action → 404, missing header → 400 (today everything is 200). ⚠️ Check the client stub (
vite-plugin/src/actions.ts __jsmCall) first: it currently treats !response.ok as generic failure BEFORE parsing the JSON error payload — update the stub to parse the envelope on 4xx before this change lands, and keep {"error"} in 200 for one release if stub/back-compat with already-deployed modules matters (decide with the reviewer; default: envelope-on-4xx + updated stub, since branch is pre-release).
- Log a WARN for the malformed-caller cases (missing header, unknown action) — today they are invisible server-side too.
Step 3 — Tests
- e2e (
tests/ cypress, jahia-cypress tool) — the actions spec (hydrogen-tutorial / module group) gains:
cy.request WITHOUT accept header → body is the JSON envelope, non-empty; content-type application/json.
- WITH
accept: application/json → byte-identical envelope (regression for the shipped stub).
- Unknown action → 404 +
{"error":"Unknown action: ..."}; missing X-JS-Action → 400 + explanatory error.
- Existing browser-driven action specs stay green (stub path).
- Manual curl matrix from the repro section, pasted into the PR.
Acceptance criteria
Part of EPIC #698.
Repro:
The action executes (side effects and
console.infohappen),GenericActionEndpointproduces its JSON envelope, but withoutaccept: application/jsonthe response body is dropped somewhere in the render/action chain — empty 200, nothing logged. Every error path (Unknown action, missing header, validation failure) is equally invisible without the header.The shipped client stub always sends
accept: application/json, so browser calls are unaffected — but any hand-rolled caller (curl, server-to-server integration, tests) gets silent empty responses that are painful to diagnose (this evaluation initially misread it as an endpoint outage).Expected: the endpoint should return its JSON envelope regardless of the accept header (it has no other representation), or respond 406 with an explanatory body — anything but empty 200.
Technical plan (agent-executable)
Harness setup
cortex-sources→javascript-modules@ the feature branch. For the investigation step, also bring in Jahia core sources (jahia-private) read-only viacortex-sources.jahia-docker; build enginemvn install -pl javascript-modules-engine -am -DskipTests; deploy viajahia-deploy; deploy any module exposing a.action.ts(e.g. the test module) with its repo-nativeyarn deploy.Reproduction (baseline — record before/after)
Also repro the error paths without the header: unknown action name, missing
X-JS-Action— all currently empty 200s.Step 1 — Confirm the drop site (investigation, ~30 min)
The endpoint (
javascript-modules-engine-java/.../actions/GenericActionEndpoint.java) always returns anActionResultwith a JSON body. The body is dropped downstream in Jahia core's action handling — readorg.jahia.bin.Render#doAction(and theActionResult→ response serialization it performs) in the core sources to confirm the exact condition on theacceptheader /jsonResponsebehavior. Document the finding in the PR description (one paragraph, with the core file/line). Do not patch core — the fix below is module-side, so it ships with the engine.Step 2 — Fix (module-side, in
GenericActionEndpoint.doExecute)Write the envelope directly instead of delegating serialization to core:
{"data": ...}/{"error": ..., "issues": ...}).response.setStatus(...),response.setContentType("application/json;charset=UTF-8"), write viaresponse.getWriter(), flush.ActionResultvariant that makes core write nothing further — verify against core'sdoActionhandling of (a)nullresult, (b) a result with anullJSON payload; pick the one that neither 404s nor double-writes, and leave a comment citing the core behavior. If neither is clean, keep returning the JSONActionResultwhenacceptcontainsapplication/json(byte-identical to today for the stub) and direct-write only otherwise.vite-plugin/src/actions.ts__jsmCall) first: it currently treats!response.okas generic failure BEFORE parsing the JSON error payload — update the stub to parse the envelope on 4xx before this change lands, and keep{"error"}in 200 for one release if stub/back-compat with already-deployed modules matters (decide with the reviewer; default: envelope-on-4xx + updated stub, since branch is pre-release).Step 3 — Tests
tests/cypress,jahia-cypresstool) — the actions spec (hydrogen-tutorial / module group) gains:cy.requestWITHOUTacceptheader → body is the JSON envelope, non-empty; content-typeapplication/json.accept: application/json→ byte-identical envelope (regression for the shipped stub).{"error":"Unknown action: ..."}; missingX-JS-Action→ 400 + explanatory error.Acceptance criteria
.jsAction.donever returns an empty body: every path yields the JSON envelope regardless ofaccept.docs/2-guides/7-actions/README.mdgains a "calling actions without the client stub" subsection with the curl example incl. required headers.