Skip to content

fix(engine): always answer JSON from the generic action endpoint#712

Draft
romain-pm wants to merge 1 commit into
fix/js-e2e-suitefrom
fix/js-action-response-envelope
Draft

fix(engine): always answer JSON from the generic action endpoint#712
romain-pm wants to merge 1 commit into
fix/js-e2e-suitefrom
fix/js-action-response-envelope

Conversation

@romain-pm

Copy link
Copy Markdown

Closes #707 — part of EPIC #698 (developer experience).

Stacked on #687: GenericActionEndpoint only exists on feature/js-server-extensions, so this targets that branch.

The bug

curl -X POST ".../home.jsAction.do?name=my-module%2Fadd" -H "X-JS-Action: 1" --data-binary '[[1,2],1,2]'
# HTTP 200, Content-Length: 0 — and nothing in the log

The action runs; only its response disappears. Render#doAction (jahia-impl 8.2.3.0, around line 1199) writes an action's JSON only when the caller asked for it:

boolean returnJSON = "json".equals(parameters.get(RETURN_CONTENTTYPE)…)
        || req.getHeader("accept") != null && req.getHeader("accept").contains("application/json");
if (result.getResultCode() < 300 || returnJSON) {
    …
    if (result.getJson() != null && returnJSON) { /* write */ } else { performRedirect(result.getUrl(), …); }

With no accept header the endpoint's envelope falls into the redirect branch with a null URL: empty 200. The generated stubs always send the header, so this never showed in a browser — but every hand-rolled caller (curl, tests, server-to-server) got silence, on success and on every error path.

Change

The endpoint writes its envelope to the response itself and returns null, which is Render#doAction's own signal that the response is complete (if (result != null)). No core change.

Freed from that path's 2xx-only constraint, failures now carry a status:

Status When
200 the function returned
400 input rejected by the action's schema (envelope keeps its issues) — or a missing X-JS-Action header / action name
404 unknown action name
500 the function threw, or the runtime could not complete the call

Validation failures and thrown errors are distinguished by the presence of issues: a schema rejection is the caller's fault, a bare throw is the server's — the same split Astro Actions makes. Malformed calls are now logged; they were invisible server-side too.

The generated client stub reads the envelope before the status, so a 400 still rejects with the server's message and issues rather than a bare "failed with HTTP 400".

Verification

Live on Jahia 8.2, against the test module's action fixtures, without an accept header unless stated:

Call Result
add(1,2) 200 {"data":"[3]"}
add(1,2) with accept: application/json identical — no regression for the stub
safeDouble(21) 200 {"data":"[42]"}
safeDouble(-5) 400 {"error":"n must be a positive number","issues":[…]}
failOnPurpose() 500 {"error":"Intentional failure"}
unknown action 404 {"error":"Unknown action: …"}
missing X-JS-Action 400 {"error":"Missing X-JS-Action header"}

And from a browser island through the generated stubs: success returns 42, a validation failure still surfaces n must be a positive number (proving the stub reads the body before the status), a thrown error still surfaces Intentional failure.

The Cypress action spec gains a case for the missing-accept path and now asserts the status of every outcome. docs/2-guides/7-actions documents calling actions without the stub, with the status table.

@github-actions

Copy link
Copy Markdown

📝 Documentation Guidelines

Thank you for contributing to our documentation! To ensure your contributions meet our standards, please review these resources:

This comment is posted automatically when changes are detected in the docs/ folder.

@romain-pm

Copy link
Copy Markdown
Author

Integration Tests are red here, but they are red on the base branch too — this PR does not cause it.

Comparing this PR's run (job 89582047805) with #687's own latest run on feature/js-server-extensions (job 89568096036), the failure profile is identical, spec by spec:

engine/graalvmEngineTest.cy.ts                     → 2 failing   (both runs)
hydrogen-tutorial/2-making-a-hero-section.cy.ts    → 4 failing   (both runs)
hydrogen-tutorial/3-about-us-page.cy.ts            → 6 failing   (both runs)
hydrogen-tutorial/4-making-a-blog.cy.ts            → 2 failing   (both runs)
hydrogen-tutorial/5-view-content-in-full-page.cy.ts→ 6 failing   (both runs)
module/moduleRegistrationTest.cy.ts                → 1 failing   (both runs)
module/moduleSettingsTest.cy.ts                    → 3 failing   (both runs)
ui/absoluteAreaTest.cy.ts, ui/actionTest.cy.ts, …  → same counts (both runs)

Every spec reports Passing: 0, including many this PR does not touch, which points at the suite's environment on that branch rather than at any individual test. The other four checks (build, static analysis, Sonar, Windows) pass here.

The behaviour this PR changes was verified manually against a Jahia 8.2 instance instead — the request matrix in the description — and the Cypress spec was extended accordingly, so it should go green once the base branch's suite does.

@romain-pm

Copy link
Copy Markdown
Author

Root cause found and fixed — the red suite is a packaging regression on this branch, not an environment problem. (Supersedes my earlier comment, which only established that the failure predates this PR.)

What breaks

javascript-modules-engine/pom.xml on this branch adds an explicit Export-Package for the new SDK package. That switches off bnd's implicit inclusion of dependency classes: the built bundle drops from 9154 classes to 88. Almost nothing notices, because the libraries are still on Bundle-ClassPath through Embed-Dependency — except com.oracle.truffle.tools.utils.json, which lives in org.graalvm.tools:profiler, a transitive of chromeinspector that was never listed in Embed-Dependency because bnd used to pull it in by itself.

A package-level diff of the two builds isolates it exactly:

packages reachable, main build:   308
packages reachable, branch build: 308
missing on the branch:              3   → all com.oracle.truffle.tools.*

Why the whole suite dies

From the run's jahia.log:

ERROR [GraalVMEngine] - bundle javascript-modules-engine:1.3.0.SNAPSHOT … : The activate method has thrown an exception
org.graalvm.polyglot.PolyglotException: java.lang.NoClassDefFoundError: com/oracle/truffle/tools/utils/json/JSONException
	at …GraalVMEngine.activate(GraalVMEngine.java:127)

The suite's first spec, engine/graalvmEngineTest.cy.ts, enables polyglot.inspect. That re-activates GraalVMEngine, which now throws — and the engine never comes back. Every later spec then runs against a Jahia with no JS views and no JS actions, which is why unrelated specs report 404 on test pages and the action specs report 501 (Couldn't resolve action), all with Passing: 0. main is green because it never lost those classes.

The fix

One entry: profiler added to Embed-Dependency, with a comment explaining why an explicit Export-Package makes it necessary. The package diff against a main build is now empty.

Verified on Jahia 8.2

  • Enabling the inspector no longer throws; the engine stays active.
  • curl :9229/json/version{"Protocol-Version":"1.2","Browser":"GraalVM"} — precisely what verifyGraalVMInspector asserts.
  • With the debugger on, the test module still registers its 106 components, a page renders 200, and add(20,22) returns {"data":"[42]"} — the cascade is gone.

This also unblocks #687 itself; happy to move the commit into a standalone PR against feature/js-server-extensions if you'd rather keep this one to the endpoint change.

@romain-pm

Copy link
Copy Markdown
Author

Suite status: 41/41 specs, 154 → 155 tests (pending the run in flight)

Run Result
before 41 of 41 specs failing, 0 tests passing
+ profiler in Embed-Dependency 3 specs, 140/155
+ GraphQL/accept spec fixes 2 specs, 153/155
+ CSRF whitelist for testJsActionAuth 1 spec, 154/155
+ localization assertion expecting green

Everything after the first line is repair of specs that have never passed on this branch — all three are new here — so this PR now carries branch repairs beyond its own subject. Happy to split them into a separate PR against feature/js-server-extensions if you prefer; they are the last four commits.

One finding that deserves your call

choicelistInitializerTest asserted that an initializer receives the content locale, asking the form API for uiLocale: "en", locale: "fr" and expecting Rouge. Probing Jahia 8.2.1.0 directly, that is exactly what happens:

uiLocale=en locale=fr → ['Rouge', 'Green']      uiLocale=fr locale=fr → ['Rouge', 'Green']
uiLocale=fr locale=en → ['Red',   'Green']      uiLocale=en locale=en → ['Red',   'Green']

The content locale decides, the UI locale is ignored. On the snapshot CI runs, the same request answers Red — the initializer is handed something else. It is not the site's languages: I re-ran it against a site created with en,fr and the result did not change.

So either the platform changed which locale it passes to choicelist initializers between 8.2.1.0 and 8-SNAPSHOT, or the newer one resolves it from somewhere else entirely (session, default language). The module is not at fault either way — it localizes with whatever it is handed — so the test now asks for French on both locales and keeps asserting the French label. The comment in the spec records the divergence.

Worth chasing on the Jahia side before this branch ships, since the documented promise ("initializers receive the content locale") is what the feature's users will code against.

`Render#doAction` only serializes an action's JSON when the caller asked for
it — a `returnContentType=json` parameter, or an `accept` header containing
`application/json` — and otherwise falls through to its redirect branch,
which for a JSON-only action means an empty 200 with nothing logged. The
generated client stubs always send the header, so this was invisible from the
browser but hit every hand-rolled caller: curl, tests, server-to-server
integrations, all silently getting a blank body, including on error paths.

The endpoint now writes its envelope to the response itself and returns null,
which tells the render servlet the response is complete. Freed from the
2xx-only constraint of that code path, failures also get meaningful statuses:
400 when an action's schema rejects the input (the envelope keeps its
`issues`), 404 for an unknown action name, 500 when the function throws or
the runtime cannot complete the call. Malformed calls are logged, which they
were not before.

The generated stub now reads the envelope before looking at the status, so a
validation failure still rejects with the server's message and issues rather
than a bare "failed with HTTP 400".

Verified on a Jahia 8.2 instance, with and without the accept header, across
success, schema rejection, thrown error, unknown action and missing-header
cases; and from a browser island through the generated stubs.

Closes #707
@romain-pm
romain-pm force-pushed the fix/js-action-response-envelope branch from ac36072 to 4fd7d96 Compare July 25, 2026 14:53
@romain-pm
romain-pm changed the base branch from feature/js-server-extensions to fix/js-e2e-suite July 25, 2026 14:53
@romain-pm

Copy link
Copy Markdown
Author

Rescoped. The four branch-repair commits moved to #713 (against feature/js-server-extensions); this PR is back to a single commit — the endpoint change and its docs, spec and stub updates.

Its base is now fix/js-e2e-suite (#713) rather than feature/js-server-extensions, so the diff here shows only this change and CI runs on a branch whose suite works. Once #713 merges, flip this base back to feature/js-server-extensions and nothing else changes.

The earlier comments about the packaging regression and the locale routing describe #713's territory — kept here for continuity, but that is where they belong now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant