Skip to content

fix(actions)!: failures speak HTTP — rejections are 400, success is a single wrap (#3962) - #3969

Merged
os-zhuang merged 3 commits into
mainfrom
claude/global-actions-unreachable-rw7hpk
Jul 30, 2026
Merged

fix(actions)!: failures speak HTTP — rejections are 400, success is a single wrap (#3962)#3969
os-zhuang merged 3 commits into
mainfrom
claude/global-actions-unreachable-rw7hpk

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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

Outcome Before Now
Ran, returned 200 {success:true, data:{success:true, data:X}} 200 {success:true, data:X} — single wrap
Ran, rejected 200 {success:true, data:{success:false, error, code?, fields?}} 400 {success:false, error:{message, code, details:{code?, fields?}}}
Rejected flow 200 inner envelope 400, details.code: FLOW_FAILED
Never dispatched 404 / 403 / 400 / 503 (#3930/#3951) unchanged
Crashed 500 (#3951) unchanged

The 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 /data has always used, which @objectstack/client already normalizes to err.code / err.fields (#3927). The #3951 crash-vs-rejection discriminator now selects 400 vs 500.

On flipping #3937's test

actions-validation-envelope.test.ts pinned 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 reused deps.success(); /actions was 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 / invokeGlobal still 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 boolean success with 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 a success key is handler-owned and passes through untouched (test pins this).

Testing

  • Suites: runtime 837, client 196, rest 440, mcp 83, objectql 1163 — all green; build 71/71; eslint 0 errors.
  • Contract tests rewritten in place: 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).
  • Docs: error-catalog.mdx Action Errors table collapses to "the status is the signal"; ui/actions.mdx, client-sdk.mdx updated.

Migration (raw-HTTP third parties only)

Branch on the HTTP status; on 200, data is the handler's return value directly (one level less than before). Callers using @objectstack/client need 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

… 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
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 1:30am

Request Review

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
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/client, @objectstack/runtime.

26 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/client)
  • content/docs/api/client-sdk.mdx (via @objectstack/client, packages/runtime)
  • content/docs/api/data-flow.mdx (via @objectstack/client)
  • content/docs/api/environment-routing.mdx (via @objectstack/client)
  • content/docs/api/error-catalog.mdx (via @objectstack/client)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/client, @objectstack/runtime)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/client)
  • content/docs/kernel/runtime-services/index.mdx (via packages/client)
  • content/docs/permissions/authentication.mdx (via @objectstack/client, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/client, @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/client)
  • content/docs/releases/implementation-status.mdx (via @objectstack/client, @objectstack/runtime)
  • content/docs/releases/v16.mdx (via @objectstack/client)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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
@os-zhuang
os-zhuang merged commit 195ad76 into main Jul 30, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/global-actions-unreachable-rw7hpk branch July 30, 2026 01:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants