Skip to content

.jsAction.do returns empty HTTP 200 when accept header lacks application/json #707

Description

@romain-pm

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

  1. Open Cortex; cortex-sourcesjavascript-modules @ the feature branch. For the investigation step, also bring in Jahia core sources (jahia-private) read-only via cortex-sources.
  2. 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:

  1. Build the JSON envelope exactly as today ({"data": ...} / {"error": ..., "issues": ...}).
  2. response.setStatus(...), response.setContentType("application/json;charset=UTF-8"), write via response.getWriter(), flush.
  3. 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.
  4. 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).
  5. Log a WARN for the malformed-caller cases (missing header, unknown action) — today they are invisible server-side too.

Step 3 — Tests

  1. 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).
  2. Manual curl matrix from the repro section, pasted into the PR.

Acceptance criteria

  • .jsAction.do never returns an empty body: every path yields the JSON envelope regardless of accept.
  • Error paths carry meaningful HTTP status codes; shipped client stub still resolves/rejects exactly as before (stub updated in the same PR if needed).
  • Malformed calls produce a server-side WARN.
  • Core behavior documented (file/line) in the PR; no core patch required.
  • docs/2-guides/7-actions/README.md gains a "calling actions without the client stub" subsection with the curl example incl. required headers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions