Skip to content

fix(runtime,spec)!: the dispatcher's error.code is the semantic string; the HTTP status moves to httpStatus (#3842) - #3971

Merged
os-zhuang merged 3 commits into
mainfrom
claude/dispatcher-http-status-code-rhryfw
Jul 30, 2026
Merged

fix(runtime,spec)!: the dispatcher's error.code is the semantic string; the HTTP status moves to httpStatus (#3842)#3971
os-zhuang merged 3 commits into
mainfrom
claude/dispatcher-http-status-code-rhryfw

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3842. Deletes the #3687 pin, which asked to be deleted rather than updated once the dispatcher was fixed.

为什么现在可以做,而不必等 #3841

Issue 里写着 “Blocked on #3841 —— 先修这个就等于随手选了一种命名方言”。这一点值得先说清楚,因为本 PR 没有#3841 落地。

关键区分是:#3841 管的是「重命名」,本 PR 做的是「换字段」

  • 线上已经存在的每一个 code 都是原样搬运的:PERMISSION_DENIEDROUTE_NOT_FOUNDPASSWORD_EXPIREDPROJECT_MEMBERSHIP_REQUIREDVALIDATION_FAILEDunauthenticated。一个字符都没改,只是从 details.code / details.type / error.type 搬进了声明好的 error.code
  • 唯一需要新造字符串的地方,是那些本来就没有语义 code、只有一个状态码的分支 —— 而 ApiErrorSchema.code 是必填的,所以必须填点什么。这些走 spec 里唯一一张声明表(HttpStatusErrorCodeMap / standardErrorCodeForHttpStatus),取值全部来自 spec 已经声明、error-catalog.mdx 已经文档化的 StandardErrorCode。这是「照着已声明的目录取值」,不是「另立一套方言」。

结果是:#3841 要扫的面没有变大,反而变小了 —— 原来是四个站点、三个停车位,现在是一张表加一个 enum,改名时改一个文件。

顺带说明:dispatcher 今天本来就同时在发两种方言(ANONYMOUS_DENY_CODE 是小写的 unauthenticated,其余是 SCREAMING),本 PR 如实保留了这个现状,没有粉饰,也没有替 #3841 做决定。

漂移本身

HttpDispatcher.error() 把 HTTP 状态码当作 code 参数,直接写进了 ApiErrorSchema 留给语义字符串的字段:

private error(message: string, code: number = 500, details?: any) {
    return { status: code, body: { success: false, error: { message, code, details } } };
}

于是 error.code 返回 400/403/503 —— 一个数字,和 response status 重复,并且占住了调用方本该用来分支的那个槽位。真正的 code 只能另找地方,而且找了三个地方:

站点 真 code 停在
auth gate / 权限拒绝 / 匿名拒绝 details.code
project-membership gate details.type
routeNotFound() error.type —— 第三种拼法,和数字 code 并列

改成什么样

// 之前                                        // 之后
{ "error": {                                   { "error": {
    "message": "",                                "code": "PERMISSION_DENIED",
    "code": 403,                                   "message": "",
    "details": { "code": "PERMISSION_DENIED" }     "httpStatus": 403
} }                                            } }
读取 之前 之后
语义 code error.details.code / error.details.type / error.type error.code
HTTP 状态 error.code error.httpStatus(或 response status)
上下文 error.details(code 混在里面) error.details(只剩真上下文,空则不出现)

顺带对齐的:同一张网络面上的另外两个出口

Issue 的 scope note 说 “dispatcher only”。但同一个 wire surface 上其实有三个构造点,只修一个会让这张面自相矛盾,所以三个一起改了:

  1. HttpDispatcher.error() —— returned 错误
  2. HttpDispatcher.routeNotFound() —— 404(以及 dispatcher-plugin 里那份重复的内联 404)
  3. dispatcher-pluginerrorResponseBase() —— thrown 错误

第三个还顺手补了一个真 bug:它根本没读过 err.code(errorFromThrown 至少还把它塞进 details),因为那个字段被状态码占着,没地方放。所以同一个 SDK 方法,由哪个出口作答会决定调用方能不能拿到 code。现在两个出口一致了 —— 这正是 #3636#3675#3689 一层层在收的那个不对称。

另外 domains/ai.ts 有两处手搓 envelope、domains/mcp.ts 的 405 有一处(它需要自己的 Allow header),也一并接进同一个 builder。

Spec 改动

  • ApiErrorSchema 新增可选 httpStatus。先例是 EnhancedApiErrorSchema.httpStatus加法,不破坏。
  • StandardErrorCode 新增 method_not_allowed / precondition_required —— runtime 实际会返回、而 enum 叫不出名字的两个状态(405 / 428)。没有它们,405 会掉进 4xx 兜底桶被报成校验失败。加法,不破坏。
  • BREAKING DispatcherErrorCode:成员从 '404' | '405' | '501' | '503'(HTTP 状态码的字符串写法,当初就是为了 match 数字 code)改为被移除的 error.type 所声明的那四个语义拼法,原样搬运。FROM DispatcherErrorCode.parse('404') → TO DispatcherErrorCode.parse('ROUTE_NOT_FOUND');要匹配状态码请读 error.httpStatus。TypeScript 会报出每一处调用点。
  • BREAKING DispatcherErrorResponseSchema:error.code 变成 z.string()(原 z.number().int()),error.type 移除(并入 code),httpStatus / details 补声明。这张 schema 是漂移的根源 —— 它对同一个字段声明了和 ApiErrorSchema 相反的东西,等于给偏离发了许可证。FROM { code: 404, type: 'ROUTE_NOT_FOUND' } → TO { code: 'ROUTE_NOT_FOUND', httpStatus: 404 }

消费方梳理(逐个查过,不是假设)

  • SDK —— 无需改动,且这正是 Prime Directive Add comprehensive test suite for Zod schema validation #12 说的「在生产端治」。 ObjectStackClient 早就为这个 bug 挂了 shim(asSemanticCode(errorBody?.error?.details?.code) ?? asSemanticCode(errorBody?.error?.code)),注释里还专门写了 “error.code in the WRAPPED form is the HTTP status”。生产端修好后这句注释不再成立,已改写。shim 本身保留:SDK 构建和它连的 server 是独立发版的,新 SDK 连老 server 时仍要能在老位置找到 code —— 和 fix(service-storage,service-i18n): emit the declared error envelope, not a bare { error } (#3675) #3687 里 console 读双方言的理由一样,这是版本边界上的容忍,不是债。
  • objectui —— 无需改动,而且是从坏变好。 全仓查过:error.code 的读取方(marketplaceApisuggestedBindingsApiCloudConnectionPanelSettingsView)全都期望语义字符串(payload?.error?.code ?? \HTTP_${res.status}`=== 'SETTINGS_LOCKED'),今天从 dispatcher 拿到的是数字;修完才是它们一直以为自己在读的东西。details.code 只有两处读取(error-message.tsmetadata-client.ts),两处都有落到 e.code 的兜底,现在兜到语义字符串。error.type**零读取方**。没有任何一处把error.code` 和数字比较。
  • dogfood 套件 —— 无需改动:它断言的 3 条 error.code 全部来自 service-storage 路由(AUTH_REQUIRED / ATTACHMENT_DOWNLOAD_DENIED),那是 fix(service-storage,service-i18n): emit the declared error envelope, not a bare { error } (#3675) #3687 已经修好的另一个生产者。
  • plugin-authcloud-connectionrest-server:各自本来就发语义 code,不受影响 —— 反过来也说明 dispatcher 才是那个异类。

守卫

新增 packages/runtime/src/error-envelope.conformance.test.ts,双向,和 #3687 给 storage/i18n 写的那对套件同一形状:

  1. 运行时 —— 四种产生 code 的路径(按状态派生 / 从 details.code 提升 / 从 DispatcherErrorCode 拼写 / 从抛出的错误上取)各驱动一遍,外加被本 PR 收编的每一个停车位各一例,body 全部拿packages/spec import 的 BaseResponseSchema / ApiErrorSchema 去 parse —— 不是本地抄一份、会跟着漂的字面量。
  2. 源码扫描 —— 扫过整个 dispatcher 栈(http-dispatcher.tsdispatcher-plugin.tsdomain-handler-registry.tsdomains/ai.tsdomains/mcp.ts),新分支无法悄悄写回数字 code、无法复活 type-当-code 的兄弟字段,且 envelope 只能在一个地方构造。没有第 2 条,这套件只能覆盖写它那天存在的分支 —— 而四个站点漂成三个停车位,正是这么来的。

两个方向都做了 mutation check:把 domains/ai.ts 的一处改回手搓数字 envelope,扫描立刻红两条(never writes a numeric code + builds every error body through the one builder)。

Tests

Suite Result
pnpm test(turbo 全量) 132 / 132 tasks successful
@objectstack/runtime 866 passed (63 files)
@objectstack/spec 6863 passed (263 files)
@objectstack/client 196 passed (14 files)
@objectstack/rest 440 passed (30 files)
@objectstack/dogfood(真实 stack) 400 passed, 3 skipped

另外跑过 check:authorable-surface(api/ApiError:httpStatus 已入 ratchet 并提交)、check-doc-authoringcheck-nul-bytes、eslint。

文档

  • wire-format.mdx §7 新增 “Dispatcher routes use it too” —— 之前只描述了 service-mounted 路由的信封,dispatcher 那份从来没写过。
  • api/index.mdx:那段 “where code is the numeric HTTP status” 的示例已更新。
  • client-sdk.mdx / error-catalog.mdx:两处描述旧形状的段落已更新;catalog 的 code 计数 51 → 53,并新增 “Request Errors (405/428)” 小节。
  • releases/v17.mdx:新增 breaking + migration 条目。
  • docs/audits/2026-07-dispatcher-client-route-coverage.md §12:dispatcher 那一行从「pinned」改为「moved」,并记下这枚 pin 到底买到了什么 —— 它在修复落地的那一刻就红了,而且精确点名了唯一那个字段。

留在原地的

🤖 Generated with Claude Code

https://claude.ai/code/session_018u8vDQLWPJxBWQESznDpSj


Generated by Claude Code

…es to httpStatus (#3842)

`HttpDispatcher.error()` took the HTTP status as its `code` argument and wrote
it into the field `ApiErrorSchema` reserves for a semantic string, so
`error.code` came back as 400/403/503 — a number duplicating the response
status and occupying the one slot a caller branches on. The real code then had
to go elsewhere, and did, three elsewheres: `details.code`, `details.type` and
`error.type`. Four sites, three parking spots, because the declared one was
full.

`code` is now the semantic string, `httpStatus` carries the number, and
`details` is genuine context only. Every code already on the wire moves
verbatim — PERMISSION_DENIED, ROUTE_NOT_FOUND, PASSWORD_EXPIRED,
PROJECT_MEMBERSHIP_REQUIRED, VALIDATION_FAILED, unauthenticated. This moves a
field; #3841 still owns renaming any of them. A branch with no code of its own
gets one derived from the status via one declared map in the spec
(`HttpStatusErrorCodeMap` / `standardErrorCodeForHttpStatus`), so the value is
a catalogued `StandardErrorCode` rather than an invented string, and #3841's
sweep is one file.

Spec:
- `ApiErrorSchema` gains optional `httpStatus` (precedent:
  `EnhancedApiErrorSchema.httpStatus`). Additive.
- `StandardErrorCode` gains `method_not_allowed` / `precondition_required` —
  the two statuses the runtime returns that the enum could not name. Additive.
- BREAKING `DispatcherErrorCode`: `'404'|'405'|'501'|'503'` becomes the four
  semantic spellings the removed `error.type` declared, moved verbatim.
- BREAKING `DispatcherErrorResponseSchema`: `code` is a string, `type` is gone,
  `httpStatus`/`details` are declared. This schema declared the opposite of
  `ApiErrorSchema` for the same field, which is what legitimised the deviation.

Also aligned, being the same wire surface: `dispatcher-plugin`'s
`errorResponseBase` (which used to discard a thrown error's `.code` outright,
having nowhere to put it) and its inline 404, plus the MCP 405. All bodies now
come from one builder, guarded both ways by `error-envelope.conformance.test.ts`
— every branch driven and parsed against the schema imported from the spec,
plus a source scan so a new branch cannot reintroduce a numeric `code`.

Deletes the #3687 pin, which asked to be deleted rather than updated once the
dispatcher was fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018u8vDQLWPJxBWQESznDpSj
@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 12:23am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/xl labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/client, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/client, packages/runtime, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/client)
  • content/docs/api/environment-routing.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/runtime, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • 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/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/client, @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/client)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/client, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/client, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/client, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/client)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/client, @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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.

claude added 2 commits July 30, 2026 00:19
…nges

`content/docs/references/` is generated from `packages/spec` by
`scripts/build-docs.ts` and is gated by `check:docs`. The spec edits in the
parent commit — `ApiError.httpStatus`, two new `StandardErrorCode` members, and
`DispatcherErrorCode`/`DispatcherErrorResponseSchema` — left 12 reference pages
stale. Regenerated with `gen:schema && gen:docs`; no hand edits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018u8vDQLWPJxBWQESznDpSj
…pshot (#3842)

Two more artifacts generated from `packages/spec` and gated in CI, missed
alongside `content/docs/references/`:

- `skills/objectstack-api/references/_index.md` (`check:skill-refs`)
- `packages/spec/api-surface.json` (`check:api-surface`) — records the two
  intentional additions, `HttpStatusErrorCodeMap` and
  `standardErrorCodeForHttpStatus`. 0 breaking, 2 added.

Ran every generate/check pair in the package this time rather than one CI round
at a time: docs, skill-refs, skill-docs, api-surface, spec-changes,
upgrade-guide, react-blocks, authorable-surface, liveness, empty-state,
react-conformance, skill-examples — all twelve pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018u8vDQLWPJxBWQESznDpSj
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 00:35
@os-zhuang
os-zhuang merged commit 03d26f7 into main Jul 30, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/dispatcher-http-status-code-rhryfw branch July 30, 2026 00:36
os-zhuang pushed a commit that referenced this pull request Jul 30, 2026
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
os-zhuang added a commit that referenced this pull request Jul 30, 2026
… single wrap (#3962) (#3969)

Fixes #3962 — the platform decision classifying the 200-on-failure wire as a
bug, not a contract. The 200-with-inner-envelope shape was never designed: 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.
Five defects traced back to that one extra layer.

Contract now, identical to /data:
- ran, returned        → 200 {success:true, data: <handler return value>} (single wrap)
- ran, rejected        → 400, semantic code on error.code (VALIDATION_FAILED with
                         fields[] in details; FLOW_FAILED for a rejected flow)
- never dispatched     → 404 / 403 / 400 / 503 (unchanged, #3930/#3951)
- crashed              → 500 (unchanged, #3951; name-based discriminator now
                         selects 400 vs 500)

actions-validation-envelope.test.ts pinned the 200 "so flipping it later is a
conscious, documented break"; #3962 is that decision and the flipped test cites
it. Integrates ADR-0110 (#3958/#3987 — name identity, undeclared refusal with
no opt-out) and #3971 (semantic error.code, details.code promotion) from main.

client.actions.invoke/invokeGlobal still never throw: every failure status
folds into {success:false, error}, success reads the single wrap, and a narrow
legacy heuristic (boolean success, no foreign keys) keeps a current SDK correct
against pre-#3962 servers.

Migration (raw-HTTP callers): branch on the status; on 200, data is the
handler's return value directly. SDK callers need no change.
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The dispatcher puts the HTTP status in error.code and parks the real code in details — pinned in #3687, still unfixed

2 participants