fix(actions)!: failures speak HTTP — rejections are 400, success is a single wrap (#3962) - #3969
Merged
Merged
Conversation
… single wrap (#3962) The 200-with-inner-envelope wire was never a designed contract: no ADR or doc specified it, it originated as the catch block reusing deps.success(), and /actions was the only route of 12 that double-wrapped. #3962 (platform decision: third-party-facing API, normalized results) classifies it as a bug. Contract now, identical to /data: 200 single wrap (data = handler return value); deliberate rejection (body throw / flow rejection / ValidationError) 400 via errorFromThrown with code/fields in details; dispatch failures 404/403/400/503 and crashes 500 unchanged (#3930/#3951). A rejected flow regains code FLOW_FAILED at 400. The #3951 name discriminator now selects 400 vs 500. actions-validation-envelope.test.ts is flipped citing #3962 — the previous revision pinned 200 explicitly so that flipping it would be "a conscious, documented decision, not a side effect"; #3962 is that decision. client.actions.invoke/invokeGlobal still never throw: they fold every failure status into {success:false, error}, read the single wrap on success, and keep a NARROW legacy heuristic (boolean success + no foreign keys) so a current SDK still folds pre-#3962 double-wrapped 200s. Migration for raw-HTTP callers: branch on the status; on 200, data is the handler's return value directly. SDK callers need no change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AvZj6cLX7APd7roh2eK4F
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Integrates two main-side changes that landed mid-flight, both touching the same contract this branch changes: - ADR-0110 (#3958): action identity is `name`, undeclared executables refuse. Its new tests asserted the pre-#3962 double envelope; updated to the single wrap, and its handler-rejection case ("NOT a 404 routing miss") now asserts the 400 — the distinction it exists to pin is 400-vs-404, unchanged. - #3971: the dispatcher's `error.code` is the semantic string and `details.code` is PROMOTED into it. Assertions on `error.details.code` moved to `error.code`; `fields` stay in `details`. Conflicts resolved in ui/actions.mdx (kept main's name-vs-target paragraph + this branch's "failures speak HTTP" contract) and the type-dispatch test (kept main's ADR-0110 undeclared/valve/degraded cases, adjusted the valve run's envelope to the single wrap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AvZj6cLX7APd7roh2eK4F
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 26 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Integrates #3987 (ADR-0110 D3 revised): the undeclared-action refusal has no opt-out — OS_ALLOW_UNDECLARED_ACTIONS is retired. The conflict was this branch's #3962 single-wrap adjustment inside the old valve test, which #3987 rewrote into "refuses regardless of the retired flag"; main's side taken verbatim, as the valve semantics the adjustment belonged to no longer exist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AvZj6cLX7APd7roh2eK4F
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3962 — the platform decision (maintainer: third-party-facing API, normalized results are a hard requirement) that classifies the 200-on-failure wire as a bug, not a contract.
What changes
{success:true, data:{success:true, data:X}}{success:true, data:X}— single wrap{success:true, data:{success:false, error, code?, fields?}}{success:false, error:{message, code, details:{code?, fields?}}}details.code: FLOW_FAILEDThe inner envelope's only job was to carry the failure signal the status code was refused; with failures speaking HTTP it loses its reason to exist, so the double wrap goes with it. A validation rejection carries
details.code: 'VALIDATION_FAILED'+details.fields[]— the exact payload #3937 fought for, now on the same wire shape/datahas always used, which@objectstack/clientalready normalizes toerr.code/err.fields(#3927). The #3951 crash-vs-rejection discriminator now selects 400 vs 500.On flipping #3937's test
actions-validation-envelope.test.tspinned the 200 explicitly "so flipping it later is a conscious, documented break, not a side effect". #3962 is that documented decision — the provenance is in the issue (no ADR/doc ever specified the 200; the route's catch block reuseddeps.success();/actionswas the only route of 12 that double-wrapped; both premises the pin cited were verified false). The flipped test cites #3962 in place.Client SDK
client.actions.invoke/invokeGlobalstill never throw. They fold every failure status into{success:false, error}(already shipped), read the single wrap on success, and keep a narrow legacy heuristic — a booleansuccesswith no keys beyond the envelope's own — so a current SDK talking to a pre-#3962 server still folds the old double-wrapped 200s. A handler value that merely contains asuccesskey is handler-owned and passes through untouched (test pins this).Testing
actions-validation-envelope.test.ts(400 +details),actions-fault-vs-rejection.test.ts(the 400/500 line),actions-global-key.test.ts+actions-type-dispatch.test.ts(single wrap).error-catalog.mdxAction Errors table collapses to "the status is the signal";ui/actions.mdx,client-sdk.mdxupdated.Migration (raw-HTTP third parties only)
Branch on the HTTP status; on 200,
datais the handler's return value directly (one level less than before). Callers using@objectstack/clientneed no change. Companion console PR: objectstack-ai/objectui (same branch name).🤖 Generated with Claude Code
https://claude.ai/code/session_011AvZj6cLX7APd7roh2eK4F
Generated by Claude Code