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
[17.0.0-rc] /actions reports business failures as HTTP 200 inside a double envelope — the only route of 12 that does; unify on real status codes before GA #3962
Platform decision (maintainer): ObjectStack is a third-party-facing platform; normalized API results are a hard requirement. This issue is the "conscious, documented decision" that #3937's contract test explicitly invited ("flipping it later should be a conscious, documented decision") — the 200-on-failure behavior is classified as a bug, not a contract.
Why it is a bug and not a contract
Provenance, verified:
No requirement source exists. No ADR (of 110+), no issue, no spec document ever specified that /actions failures return 200. Until last week the docs never described the failure shape at all.
It originated as an implementation detail — the route's catch block reused deps.success() as its serializer. Across all 12 dispatcher domains (~85 deps.success call sites), /actions is the only route that wraps its real payload in a second {success, data} envelope.
redirectUrl never fired, opensInNewTab tab spun forever (objectui#2967)
payload one level deeper than every reader assumed
Marketplace could report a failed install as installed
same missed inner envelope
Client SDK envelope divergence (#3927 exists purely to normalize it)
/actions shape ≠ /data shape
Handler crashes invisible to gateways/APM/alerting (fixed for crashes in #3951)
200 carries no failure signal
The inner success flag's only job is to carry the failure signal that the status code was refused. The double envelope and the 200 are two faces of one decision; removing one removes the other.
Target contract (identical to /data)
Outcome
HTTP
Body
Ran, returned
200
{success: true, data: <handler return value>} — single wrap
The crash-vs-rejection discriminator from #3951 (error name) stays — it now selects 400 vs 500 instead of 200 vs 500. A rejected flow regains code: FLOW_FAILED at 400.
Flip actions-validation-envelope.test.ts citing this issue — the documented decision it asked for
@objectstack/client: normalizeActionResult reads the single wrap; narrow legacy-shape heuristic keeps a current SDK working against a pre-fix server; surface stays non-throwing
objectui interpretActionResponse: same single-wrap read + legacy heuristic; ActionResult.data depth self-heals on new servers
Docs: error-catalog.mdx Action Errors table collapses to "status is the signal"; ui/actions.mdx, client-sdk.mdx
Changesets with an explicit BREAKING migration note for raw-HTTP third parties: branch on the status code; data unwraps one level less
Migration (third parties using raw HTTP): a failed action is no longer a 200 — read the status; on success, data is the handler's return value directly. Callers going through @objectstack/client need no change.
17.0.0-rc is the window: this is the last cheap moment to fix the wire before GA.
Platform decision (maintainer): ObjectStack is a third-party-facing platform; normalized API results are a hard requirement. This issue is the "conscious, documented decision" that #3937's contract test explicitly invited ("flipping it later should be a conscious, documented decision") — the 200-on-failure behavior is classified as a bug, not a contract.
Why it is a bug and not a contract
Provenance, verified:
/actionsfailures return 200. Until last week the docs never described the failure shape at all.deps.success()as its serializer. Across all 12 dispatcher domains (~85deps.successcall sites),/actionsis the only route that wraps its real payload in a second{success, data}envelope.actions-validation-envelope.test.tsas a back-compat measure while fixingfields[]pass-through. Both premises it cited do not hold:data.success" — three in-repo callers did not (the console's main action path,marketplaceApi.installPackage, the cloud-connection install proxy), which is exactly the symptom [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 was filed about.Harm to date — five bugs, one root cause
success: trueread as successredirectUrlnever fired,opensInNewTabtab spun forever (objectui#2967)/actionsshape ≠/datashapeThe inner
successflag's only job is to carry the failure signal that the status code was refused. The double envelope and the 200 are two faces of one decision; removing one removes the other.Target contract (identical to
/data){success: true, data: <handler return value>}— single wrap{success: false, error: {message, code, details: {code?, fields?}}}TypeError, driver error, sandbox timeout)The crash-vs-rejection discriminator from #3951 (error
name) stays — it now selects 400 vs 500 instead of 200 vs 500. A rejected flow regainscode: FLOW_FAILEDat 400.Implementation checklist
domains/actions.ts: success exits single-wrap (deps.success(result)); deliberate rejections exit viaerrorFromThrown(err, 400)(carriesVALIDATION_FAILED+fields[]intodetails, same as every other domain since fix(runtime): route every domain catch through errorFromThrown (#3918 follow-up) #3925)actions-validation-envelope.test.tsciting this issue — the documented decision it asked for@objectstack/client:normalizeActionResultreads the single wrap; narrow legacy-shape heuristic keeps a current SDK working against a pre-fix server; surface stays non-throwinginterpretActionResponse: same single-wrap read + legacy heuristic;ActionResult.datadepth self-heals on new serverserror-catalog.mdxAction Errors table collapses to "status is the signal";ui/actions.mdx,client-sdk.mdxdataunwraps one level lessMigration (third parties using raw HTTP): a failed action is no longer a 200 — read the status; on success,
datais the handler's return value directly. Callers going through@objectstack/clientneed no change.17.0.0-rc is the window: this is the last cheap moment to fix the wire before GA.