You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[17.0.0-rc.0] Global actions unreachable: registered under key 'global', REST fallback probes '*' — and handler failures return HTTP 200 {success:true,data:{success:false}} #3913
packages/objectql/src/plugin.ts:1345-1350 (actionObjectKey) → 'global', used at :1550
But the REST dispatcher's fallback probes '*' (packages/runtime/src/domains/actions.ts:195-204), and executeAction is an exact-string Map lookup with no wildcard semantics (packages/objectql/src/engine.ts:731-746 — the error template there is verbatim the observed Action 'log_call' on object '*' not found).
Result: POST /api/v1/actions/<any-object>/log_call always fails. POST /api/v1/actions/global/log_call works by accident (path segment matches the registration key). Doc comments disagree with each other about which key is canonical (domains/actions.ts:15/:39 say '*'; objectql/src/plugin.ts:1348 and action-execution.ts:694-698 say 'global').
Defect 2 — every handler failure is wrapped as transport success
Both the success and failure paths call deps.success(...) (domains/actions.ts:205 / :215), and success() always emits {status: 200, body: {success: true, data}} (packages/runtime/src/http-dispatcher.ts:444-449). Wire result for any handler error (including the FORBIDDEN from the ctx.api issue filed separately):
{"success":true,"data":{"success":false,"error":"Action 'log_call' on object '*' not found"}}
The client SDK documents this envelope and deliberately does not throw (packages/client/src/index.ts:2882/:4304) — so every caller that doesn't manually check data.success silently swallows failures. The shipped console does exactly that on its main action path (objectui issue to follow), so global-action failures produce a green success toast.
Suggested fix direction
Pick one canonical key ('global'), make the fallback probe it (or add wildcard semantics in executeAction), and route handler failures through deps.error with a real status code.
#3930 fixes Defect 1 in full and takes only the uncontested half of Defect 2. Flagging why, because this issue's suggested fix and #3937 point in opposite directions and I don't think a merge conflict is the right place to settle that.
Defect 1 — 'global' is now the canonical key, one probe order ([routed object, 'global', '*']) shared by the REST route and the MCP bridge, /actions//:action routes instead of 400-ing.
Defect 2 — #3937 landed shortly before #3930 and pinned the opposite contract in actions-validation-envelope.test.ts:
The status stays 200./actions has always reported business failure in the payload ({ success: false }), not as a transport error, and every caller branches on data.success. […] That choice is asserted below so it cannot drift silently.
#3930 now honours that, and moves only the exit where nothing dispatched: an action registered under no key is a 404. That isn't a new contract — the route already answers 403 denied, 400 wrong action type and 503 unavailable with a status. The line is did a handler run: below it the payload, above it the status. That alone fixes the case in this report, since Action 'log_call' … not found was exactly a non-dispatch.
On the wider question, for whoever settles it
Two of #3937's four premises look weaker than they read, and I'd rather put that on the record than quietly work around it:
"/actions has always reported business failure in the payload." Only the catch block does. The route already returns 405, 400 (bad path), 403 (ADR-0066 D4 gate), 400 (wrong action type), 503 (no engine / no automation). The contract is already split at the dispatch boundary — the question is whether that boundary is in the right place, not whether a uniform contract is being broken.
"Every existing caller branches on data.success." This is the one I'd push back on hardest, because it's checkable and it doesn't hold. Three callers in these two repos did not:
marketplaceApi.installPackage — same hole, could report a package as installed when it was not.
cloud-connection-plugin's install proxy — forwards resp.status verbatim, so it forwarded a 200.
RecordDetailViewdid check it — and its comment says why it had to be taught: "otherwise a failed action is mistaken for success and fires the green 'completed' toast while the real error is swallowed." Someone already hit this once. That's the signature of a default whose misuse is silent: the code that forgets looks identical to the code that doesn't, until a user is told an operation succeeded.
Two more things worth weighing, neither of which is about HTTP purity:
A 200 is invisible to everything between client and server. Gateways, retry/circuit-breaker policy, error-rate alerting, APM auto-capture, uptime probes, fetch().ok, agent integrations over MCP — all read status. For a platform whose main extension surface is customer-authored script bodies, "customer action bodies are throwing" currently has no signal that doesn't require body-parsing at every hop.
The costs are asymmetric: treating a business rejection as a failure loses nothing (it was a failure); treating a failure as success ships a green toast over a broken operation.
None of that makes #3937 wrong about the case it fixed — a form action's ValidationError genuinely is a normal outcome, and code/fields in the payload is a real improvement. It's the scope of the 200 I'd question, and 17.0.0-rc is the moment to question it. Happy to do that as a follow-up PR if there's appetite; #3930 deliberately doesn't presume the answer.
Activity
Merge origin/main into claude/global-actions-unreachable-rw7hpk
os-zhuang commentedon Jul 29, 2026
#3930 fixes Defect 1 in full and takes only the uncontested half of Defect 2. Flagging why, because this issue's suggested fix and #3937 point in opposite directions and I don't think a merge conflict is the right place to settle that.
Defect 1 —
'global'is now the canonical key, one probe order ([routed object, 'global', '*']) shared by the REST route and the MCP bridge,/actions//:actionroutes instead of 400-ing.Defect 2 — #3937 landed shortly before #3930 and pinned the opposite contract in
actions-validation-envelope.test.ts:#3930 now honours that, and moves only the exit where nothing dispatched: an action registered under no key is a 404. That isn't a new contract — the route already answers 403 denied, 400 wrong action type and 503 unavailable with a status. The line is did a handler run: below it the payload, above it the status. That alone fixes the case in this report, since
Action 'log_call' … not foundwas exactly a non-dispatch.On the wider question, for whoever settles it
Two of #3937's four premises look weaker than they read, and I'd rather put that on the record than quietly work around it:
"
/actionshas always reported business failure in the payload." Only the catch block does. The route already returns 405, 400 (bad path), 403 (ADR-0066 D4 gate), 400 (wrong action type), 503 (no engine / no automation). The contract is already split at the dispatch boundary — the question is whether that boundary is in the right place, not whether a uniform contract is being broken."Every existing caller branches on
data.success." This is the one I'd push back on hardest, because it's checkable and it doesn't hold. Three callers in these two repos did not:useConsoleActionRuntime.serverActionHandler— the console's main action path (list toolbars, row actions, page actions). Checkedres.okand the outer envelope only, so a failure fired the green "completed" toast. This is the symptom in this report; fixed in fix(actions): a failed server action no longer reports as success (green toast) objectui#2963.marketplaceApi.installPackage— same hole, could report a package as installed when it was not.cloud-connection-plugin's install proxy — forwardsresp.statusverbatim, so it forwarded a 200.RecordDetailViewdid check it — and its comment says why it had to be taught: "otherwise a failed action is mistaken for success and fires the green 'completed' toast while the real error is swallowed." Someone already hit this once. That's the signature of a default whose misuse is silent: the code that forgets looks identical to the code that doesn't, until a user is told an operation succeeded.Two more things worth weighing, neither of which is about HTTP purity:
ValidationErroris 400 +fields[]on/data(mapDataError, always has been) and 200 on/actions. That divergence has already cost something concrete — fix(client): normalize both server error envelopes so err.code / err.fields mean one thing (#3918 follow-up) #3927 exists purely to normalize the two envelopes client-side. And fix(runtime): route every domain catch through errorFromThrown (#3918 follow-up) #3925 just moved every other domain catch ontoerrorFromThrown;/actionsis now the single carve-out, which is where two-envelope problems come from.fetch().ok, agent integrations over MCP — all read status. For a platform whose main extension surface is customer-authored script bodies, "customer action bodies are throwing" currently has no signal that doesn't require body-parsing at every hop.The costs are asymmetric: treating a business rejection as a failure loses nothing (it was a failure); treating a failure as success ships a green toast over a broken operation.
None of that makes #3937 wrong about the case it fixed — a form action's
ValidationErrorgenuinely is a normal outcome, andcode/fieldsin the payload is a real improvement. It's the scope of the 200 I'd question, and 17.0.0-rc is the moment to question it. Happy to do that as a follow-up PR if there's appetite; #3930 deliberately doesn't presume the answer.Generated by Claude Code
fix(actions): reach global actions at their real registration key, an…
fix(actions): an action that CRASHED is a 500, not a 200 reporting su…
merge: origin/main into ADR-0110 branch
fix(actions): mount the object-less /actions//:action shape; bump obj…
fix(showcase): translate the object-less action, restoring the i18n r…
feat(showcase): add the object-less (global) action specimen
fix(showcase): translate the object-less action, restoring the i18n r…
fix(cli): dedup lint actions on the composite engine key, not the bar…
test(dogfood,runtime): reverse the E2E pin that kept a hook's `TypeEr…
fix(rest): a crashing hook body answers the sanitised fault envelope,…