fix(rest): 未分类的路由错误回消毒 5xx,不再把服务端故障说成 400 客户端错误 (#5489) - #5585
Merged
Conversation
`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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 18 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 5, 2026 19:54
baozhoutao
enabled auto-merge
August 5, 2026 19:54
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 #5489
前提复核(先证后改)
在最新 main(
c113690)上按 issue 给的复现法翻转断言,实测:前提成立。
mapDataError的终局兜底仍是return { status: 400, body: { error: raw || 'Bad request' } };。改了什么
packages/rest/src/rest-server.ts的终局兜底 —— 所有code匹配、显式状态直通、文本启发式全部放弃之后的那一支 —— 改为一个消毒过的服务端故障信封:两半都错在同一个方向:
matchEndpoint按契约抛错(它抛就是为了让 outage 不伪装成「本部署没有声明任何 endpoint」,ADR-0110 D3),或者干脆是处理器自身的TypeError。两者调用方都修不了,且都应该重试。looksLikeInternalErrorLeak什么都没匹配上,而 sys_metadata 不可用被 mapDataError 的 unknown-object 启发式误报成 404 OBJECT_NOT_FOUND,且 404 属「预期状态」因而一行日志都不留 #5462 已经记过「关键词启发式沉默不等于安全」。沿用 fix(rest): a declared 5xx no longer ships its own message to the client (#5437) #5464 的logWithheldServerFault纪律:原文进服务端日志、不进客户端(500 不在isExpectedDataStatus内,handleRouteError会打印完整错误对象;sendError那侧由logWithheldServerFault兜住)。测绘:今天有哪些真客户端错误依赖这条兜底拿 400?
一个都没有。 这是本单最大的回归风险,所以改动前先答了它,而且是实测答的:给终局兜底加桩记录每一个到达它的错误,跑完
@objectstack/rest全套(48 文件 / 719 用例)。到达的只有 6 个:metadata store unreachableconnect ECONNREFUSED 10.0.0.5:5432 (internal pool)(status: 502)Cannot read properties of undefined (reading 'name')boom(TypeError)与静态读法一致:要走到这一支,错误必须同时没有 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_ERRORDATA_STORE_FAULT()(#5462 / PR #5530)的500 {error:'Internal data error', code:'DATABASE_ERROR'}用在证据指名了存储故障的地方:驱动的 missing-relation 措辞、looksLikeInternalErrorLeak命中。而这一支的定义性事实是没有任何证据 —— 把处理器的TypeError报成DATABASE_ERROR,会把运维指向一个其实健康的数据库,和「无证据就别下结论」正好相反。这不是第三套措辞:
INTERNAL_ERROR是standardErrorCodeForHttpStatus(500)的取值(@objectstack/spec的HttpStatusErrorCodeMap),目录自己为「500 且无更具体 code」定义的下限;message 复用的是resolveErrorResponse声明式 5xx 分支(#5464)已在用的INTERNAL_ERROR_MESSAGE。两个常量、两条判据,各自说得清自己在断言什么。反向验证(方向先判后跑)
预判 —— 普通的红:还原
return { status: 400, body: { error: raw || 'Bad request' } };,新钉的用例(断言 500 /INTERNAL_ERROR/ message 被扣下)全红;「真 4xx 一个未动」那一整块保持绿 —— 那块存在的意义就是抓反向过度,即把客户端错误升格成服务端故障的「修法」。实测,与预判一致:
「真 4xx 一个未动」那一块的用例一条都没有出现在红名单里。
测试面的三种处置(逐条判,不批量重拼)
rest-endpoint-surfaces-served-only.test.ts的reports a store outage rather than an empty declaration set从>= 400改为>= 500+ 钉INTERNAL_ERROR。fix(rest): 两个端点契约面只宣告匹配器实际会服务的集合 (#5224) #5487 的注释写明了它在等本单。rest-expected-error-logging.test.ts的an UNRECOGNISED error (handler bug) stays loud …。它钉的不变式是「真处理器 bug 永远不静默」,原先靠「无code的 400 不在预期表里」成立,现在靠「500 根本不在isExpectedDataStatus的段位里」成立 —— 由句子变成结构。isExpectedRouteError的 docblock 同步改正(它原文以那条已失效的兜底为论据)。rest.test.ts的does NOT pass through an explicit 5xx status与rest-4xx-message-truncation.test.ts的5xx 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 也能把原文送达」的那半句)—— 都是描述旧兜底行为的现在时陈述,已失效。消费半径清扫
mapDataError是export的,但仓内除rest-server.ts自身与packages/rest的测试外没有第二个导入方(已全仓 grep);packages/spec/packages/client里带RestServer字样的测试是 config schema 与静态信封 fixture,与本支无关。文档content/docs/api/*没有把这条终局兜底的 400 写成契约。测试
pnpm --filter @objectstack/rest test→ 49 文件 / 737 用例全绿。packages/rest没有typecheckscript(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 全部 successpnpm 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 跳过。@objectstack/restpatch,状态码行为变化写了升级须知。边界
未动
served-endpoints.ts语义(#5487 刚落);未触packages/runtime/**、packages/cli/src/commands/**、packages/client/**;与 #5456 文件级不相交;未动content/docs/releases/。界外发现
finding,无pm:queue):mapDataError的显式状态直通只覆盖 4xx,而resolveErrorResponse覆盖 400–599,于是数据路由(直调mapDataError)上一个声明了 502/503 的生产者拿不回自己的状态码。本 PR 已把这条组合的泄漏半边封掉(现在是消毒 500),残留的只有状态码保真度;仓内暂未测到活体生产者,故按观察类立单。getMetaItem的裸 catch 把「sys_metadata 不可达」吞成「该项不存在」)是同一片领地的另一半,本 PR 不修它 —— 那是上游 catch 造出了一个 not-found,根本走不到本支的终局兜底。Generated by Claude Code