Skip to content

fix(rest): 未分类的路由错误回消毒 5xx,不再把服务端故障说成 400 客户端错误 (#5489) - #5585

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5489-route-error-outage-status
Aug 5, 2026
Merged

fix(rest): 未分类的路由错误回消毒 5xx,不再把服务端故障说成 400 客户端错误 (#5489)#5585
baozhoutao merged 1 commit into
mainfrom
claude/issue-5489-route-error-outage-status

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5489

前提复核(先证后改)

在最新 main(c113690)上按 issue 给的复现法翻转断言,实测:

FAIL  src/rest-endpoint-surfaces-served-only.test.ts > reports a store outage rather than an empty declaration set
AssertionError: expected 400 to be greater than or equal to 500
 ❯ src/rest-endpoint-surfaces-served-only.test.ts:274:28

前提成立。mapDataError 的终局兜底仍是 return { status: 400, body: { error: raw || 'Bad request' } };

改了什么

packages/rest/src/rest-server.ts 的终局兜底 —— 所有 code 匹配、显式状态直通、文本启发式全部放弃之后的那一支 —— 改为一个消毒过的服务端故障信封:

500 {"error":"Internal server error","code":"INTERNAL_ERROR"}

两半都错在同一个方向:

测绘:今天有哪些真客户端错误依赖这条兜底拿 400?

一个都没有。 这是本单最大的回归风险,所以改动前先答了它,而且是实测答的:给终局兜底加桩记录每一个到达它的错误,跑完 @objectstack/rest 全套(48 文件 / 719 用例)。到达的只有 6 个:

raw 性质
metadata store unreachable 本单的存储 outage
connect ECONNREFUSED 10.0.0.5:5432 (internal pool)(status: 502) 服务端故障,×2
Cannot read properties of undefined (reading 'name') 处理器 bug(TypeError)
boom(TypeError) 处理器 bug,×2

与静态读法一致:要走到这一支,错误必须同时没有 4xx 声明状态、不带那约 12 个被识别的 code/name、没有 innerMessage,且 message 不匹配沙箱 / provisioning / record-not-found / unknown-column / not-null / missing-relation / unknown-object / SQL-leak 任何一支。历史上唯一骑这条兜底当客户端错误的家族 —— driver-sql 无法编译的 filter 拒收 —— 已由 #4436生产者侧声明 status: 400 + INVALID_FILTER 迁走(见 sql-driver.ts 的注释),这正是契约优先带来的红利:今天这一支下面没有 4xx 形状的东西可打碎。

新测试文件里有一整个 describe 专门钉这条边界:validation / permission / feeds / attachment / unknown object / record-not-found / delete-restricted / concurrent-update / unknown-column / not-null 漂移 / 沙箱业务拒绝 / unique 冲突 —— 全部仍从各自分支拿到原本的状态码。

为什么是 INTERNAL_ERROR 而不是复用 DATABASE_ERROR

DATA_STORE_FAULT()(#5462 / PR #5530)的 500 {error:'Internal data error', code:'DATABASE_ERROR'} 用在证据指名了存储故障的地方:驱动的 missing-relation 措辞、looksLikeInternalErrorLeak 命中。而这一支的定义性事实是没有任何证据 —— 把处理器的 TypeError 报成 DATABASE_ERROR,会把运维指向一个其实健康的数据库,和「无证据就别下结论」正好相反。

这不是第三套措辞:INTERNAL_ERRORstandardErrorCodeForHttpStatus(500) 的取值(@objectstack/specHttpStatusErrorCodeMap),目录自己为「500 且无更具体 code」定义的下限;message 复用的是 resolveErrorResponse 声明式 5xx 分支(#5464)已在用的 INTERNAL_ERROR_MESSAGE。两个常量、两条判据,各自说得清自己在断言什么。

反向验证(方向先判后跑)

预判 —— 普通的红:还原 return { status: 400, body: { error: raw || 'Bad request' } };,新钉的用例(断言 500 / INTERNAL_ERROR / message 被扣下)全红;「真 4xx 一个未动」那一整块保持绿 —— 那块存在的意义就是抓反向过度,即把客户端错误升格成服务端故障的「修法」。

实测,与预判一致:

 Test Files  5 failed | 44 passed (49)
      Tests  10 failed | 727 passed (737)
     × the store-outage error this issue was raised on is a server fault
     × a handler bug lands in the same envelope — nothing here is data-specific
     × the message is WITHHELD, not truncated — this branch has no evidence it is safe
     × an error with no message at all still answers the same envelope
     × `INTERNAL_ERROR`, not `DATABASE_ERROR` — this branch cannot name a cause
     × GET /meta/:type reports a fault the caller may retry, and logs the words
     × reports a store outage rather than an empty declaration set
     × an UNRECOGNISED error (handler bug) stays loud — and is a 500, not a 400 (#5489)
     × 5xx never enters this branch at all (unchanged: sanitizing heuristics own it)
     × does NOT pass through an explicit 5xx status (message stays sanitized)

「真 4xx 一个未动」那一块的用例一条都没有出现在红名单里。

测试面的三种处置(逐条判,不批量重拼)

  • 升格:rest-endpoint-surfaces-served-only.test.tsreports a store outage rather than an empty declaration set>= 400 改为 >= 500 + 钉 INTERNAL_ERRORfix(rest): 两个端点契约面只宣告匹配器实际会服务的集合 (#5224) #5487 的注释写明了它在等本单。
  • 改写(不变式仍在,论据换了):rest-expected-error-logging.test.tsan UNRECOGNISED error (handler bug) stays loud …。它钉的不变式是「真处理器 bug 永远不静默」,原先靠「无 code 的 400 不在预期表里」成立,现在靠「500 根本不在 isExpectedDataStatus 的段位里」成立 —— 由句子变成结构。isExpectedRouteError 的 docblock 同步改正(它原文以那条已失效的兜底为论据)。
  • 升级两条「因为什么都没产出所以绿」的断言:rest.test.tsdoes NOT pass through an explicit 5xx statusrest-4xx-message-truncation.test.ts5xx never enters this branch at all 只写了 expect(r.status).not.toBe(502)。这对的落点同样成立 —— 而旧落点是 400 携带 connect ECONNREFUSED 10.0.0.5:5432 (internal pool) 全文(主机与端口都在里面)。否定断言分不出这两者,所以改为正面钉住实际落点。这也是测绘顺手挖出来的一处真实泄漏,由本 PR 一并封掉。

另外做了注释的真值维护(rest-5xx-message-sanitization.test.ts 第三行反例、rest-unknown-object-heuristic.test.ts 的反事实、sql-driver.ts 里「不声明 status 也能把原文送达」的那半句)—— 都是描述旧兜底行为的现在时陈述,已失效。

消费半径清扫

mapDataErrorexport 的,但仓内除 rest-server.ts 自身与 packages/rest 的测试外没有第二个导入方(已全仓 grep);packages/spec / packages/client 里带 RestServer 字样的测试是 config schema 与静态信封 fixture,与本支无关。文档 content/docs/api/* 没有把这条终局兜底的 400 写成契约。

测试

  • pnpm --filter @objectstack/rest test49 文件 / 737 用例全绿
  • packages/rest 没有 typecheck script(package.json 只有 build/dev/test),如实申报;代之以:
    • pnpm check:type-check-coverage → OK(62/77 包纳入 type-check,与本 PR 无关的 DEBT 台账未变动)
    • pnpm --filter @objectstack/rest --filter @objectstack/driver-sql build → 两包 tsup + DTS 全部 success
  • pnpm check:route-envelope / pnpm check:error-code-casing / node scripts/check-nul-bytes.mjs 全绿;另按纪律对本 PR 触及的全部文件做了闸门盲区自扫(grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]'),无命中。
  • pnpm --filter @objectstack/driver-sql test(注释级改动的旁证)→ 855 通过 / 44 跳过。
  • changeset:@objectstack/rest patch,状态码行为变化写了升级须知。

边界

未动 served-endpoints.ts 语义(#5487 刚落);未触 packages/runtime/**packages/cli/src/commands/**packages/client/**;与 #5456 文件级不相交;未动 content/docs/releases/

界外发现


Generated by Claude Code

`mapDataError` 的终局兜底 —— 所有 code 匹配、显式状态直通、文本启发式全部
放弃之后的那一支 —— 原先答 `{ status: 400, body: { error: <原始 message> } }`。
两半都错在同一个方向:

- 400 的语义是「你请求错了」,SDK / 代理 / 重试策略据此判定不要重试。真正落到
  这一支的恰恰相反:元数据存储读不到时 `matchEndpoint` 按契约抛错(ADR-0110
  D3,抛就是为了让 outage 不伪装成 miss),或者处理器自身的 `TypeError`。
  实测 `GET /api/v1/meta/api` 对着抛 `Error('metadata store unreachable')`
  的存储:HTTP 400。
- 原文逐字下发,而这是全文件里最没有证据可以下发的一条路径:走到这里的前提
  就是 `looksLikeInternalErrorLeak` 什么都没匹配上。

改为 `UNCLASSIFIED_FAULT()`:`500 {error:'Internal server error',
code:'INTERNAL_ERROR'}`。`INTERNAL_ERROR` 而非 `DATA_STORE_FAULT` 的
`DATABASE_ERROR` —— 后者用在证据指名了存储故障的地方,而这一支的定义性事实
是没有任何证据;`INTERNAL_ERROR` 是 `standardErrorCodeForHttpStatus(500)`
的取值,不是第三套措辞。

真客户端错误一个未动:改动前先给这一支加桩跑完 rest 全套(48 文件 / 719
用例),落到这里的只有 6 个错误 —— 本单的 outage、两个 502 的 ECONNREFUSED、
三个 TypeError,没有一个是客户端错误;历史上唯一骑这条兜底的客户端错误家族
(driver-sql 的 filter 拒收)已由 #4436 在生产者侧迁走。

- 新增 `rest-unclassified-fault-status.test.ts`:兜底落点、消毒、日志留痕、
  以及「真 4xx 全部从各自分支拿到原状态」的边界钉。
- `rest-endpoint-surfaces-served-only.test.ts` 的 outage 用例从 `>=400`
  升格为 5xx(#5487 的注释写明了在等本单)。
- `rest.test.ts` / `rest-4xx-message-truncation.test.ts` 里两条只写
  `not.toBe(502)` 的否定断言升级为钉住实际落点 —— 它们对旧的 400+原文泄漏
  同样成立,分不出两者。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
@vercel

vercel Bot commented Aug 5, 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 Aug 5, 2026 7:49pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/rest.

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

  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-sql)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-sql, @objectstack/rest)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-sql, @objectstack/rest)
  • content/docs/releases/v12.mdx (via @objectstack/rest)
  • content/docs/releases/v17.mdx (via @objectstack/rest)

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.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 5, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review August 5, 2026 19:54
@baozhoutao
baozhoutao enabled auto-merge August 5, 2026 19:54
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 91ec1ea Aug 5, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5489-route-error-outage-status branch August 5, 2026 20:13
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

Development

Successfully merging this pull request may close these issues.

/meta/:type 上一个未分类的服务端故障被报成 HTTP 400 —— handleRouteError 的兜底把 outage 说成客户端错误

2 participants