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

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.

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