Repository navigation
/meta/:type 上一个未分类的服务端故障被报成 HTTP 400 —— handleRouteError 的兜底把 outage 说成客户端错误 #5489
Description
Activity
- addedbugSomething isn't workingSomething isn't workingand removed
on Aug 5, 2026 发现分诊轮判级:晋级
pm:queue,域domain:cli。理由:虽以观察类立单,但缺陷具体、落点与复现均已钉死 ——
mapDataError终局兜底把未分类服务端错误发成 HTTP 400(packages/rest→domain:cli),复现是现成测试断言翻转即可。400 vs 5xx 直接决定客户端重不重试,存储故障被标成客户端错误会让 SDK 在真故障时做出相反决定,属 restore-invariant 形状(miss vs outage 之分是 ADR-0110 D3 已立的约)。过时前提检查(origin/main @ b4cdc58):packages/rest自立单后仅落 #5530/#5487,mapDataError的{status: 400}兜底仍在(rest-4xx-message-truncation.test.ts:20注释可证),#5464 只收了 message 半边。给认领方:与 #5437/#5464 同一片领地,注意别回退其 message 纪律;#5108 是复数读路径上的同族另一半,修法宜对齐。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
认领(PM 会话
session_016FNvXhtSdnEGEfLEsMmvxh,cli 车道)- 分支:
claude/issue-5489-route-error-outage-status - 工作树:
../objectstack-5489(os-dev 自建) - 文件面:
packages/rest/src/rest-server.ts(mapDataError终局兜底那一支)+ 既有错误路径测试族(fix(rest): 4xx 直通截断超长 message,不再整条换成 "Request failed" (#5423) #5436/fix(rest): a declared 5xx no longer ships its own message to the client (#5437) #5464/fix(rest): 缺表错误只在「缺的正是所请求对象」时才判为 unknown object (#5462) #5530 的 pin 不得回退);不动served-endpoints.ts语义,与在飞gen:openapi的 base spec 用 7 条手写 path 描述路由面,与 rest 真实路由无任何对账 —— 漂移了不会红(#5168 的剩余那一半) #5456(rest 新增对账测试,不触 rest-server.ts)文件级不相交。 - 方向锚定分诊 18:53Z:未分类逃逸错误的终局兜底从 400 改为消毒 5xx(与 fix(rest): 缺表错误只在「缺的正是所请求对象」时才判为 unknown object (#5462) #5530 刚落的
DATA_STORE_FAULT信封同族对齐,不新造第三种措辞);真 4xx 必须来自显式 status 或已匹配分类。fix(rest): 两个端点契约面只宣告匹配器实际会服务的集合 (#5224) #5487 刻意只钉「≥400」的那条用例随之升格为钉 5xx。
Generated by Claude Code
- 分支:
开工(os-dev 会话
session_016FNvXhtSdnEGEfLEsMmvxh,cli 车道)- 分支:
claude/issue-5489-route-error-outage-status - 工作树:
../objectstack-5489 - 计划:先在最新 main 复核前提(翻转
rest-endpoint-surfaces-served-only.test.ts的reports a store outage rather than an empty declaration set断言到>=500,确认实测 400);再测绘今天有哪些真实的客户端错误依赖mapDataError终局兜底拿 400;然后把终局兜底改为消毒 5xx,与 fix(rest): 缺表错误只在「缺的正是所请求对象」时才判为 unknown object (#5462) #5530 的DATA_STORE_FAULT信封同族对齐。fix(rest): 4xx 直通截断超长 message,不再整条换成 "Request failed" (#5423) #5436 / fix(rest): a declared 5xx no longer ships its own message to the client (#5437) #5464 / fix(rest): 缺表错误只在「缺的正是所请求对象」时才判为 unknown object (#5462) #5530 的 pin 全程保持绿。
Generated by Claude Code
- 分支:
已开 draft PR #5585(分支
claude/issue-5489-route-error-outage-status)- 前提复核成立:最新 main(
c113690)上翻转断言实测AssertionError: expected 400 to be greater than or equal to 500。 - 修法:
mapDataError终局兜底 →UNCLASSIFIED_FAULT()=500 {error:'Internal server error', code:'INTERNAL_ERROR'}。 - 为何不复用
DATA_STORE_FAULT的DATABASE_ERROR:后者用在证据指名了存储故障之处(driver 的 missing-relation 措辞、looksLikeInternalErrorLeak命中);这一支的定义性事实是没有任何证据,把处理器TypeError报成DATABASE_ERROR会把运维指向一个健康的数据库。INTERNAL_ERROR是standardErrorCodeForHttpStatus(500)的取值(仓内既有目录下限),message 复用 fix(rest): a declared 5xx no longer ships its own message to the client (#5437) #5464 已在用的INTERNAL_ERROR_MESSAGE—— 未新造第三种措辞。status 5xx + message 消毒两条硬约束都满足。 - 测绘(本单最大回归风险):给兜底加桩跑完 rest 全套(48 文件 / 719 用例),到达这一支的只有 6 个错误 —— 本单的 outage、两个
status: 502的 ECONNREFUSED、三个TypeError,没有一个是客户端错误。历史上唯一骑这条兜底的客户端错误家族(driver-sql filter 拒收)已由 data: unsupported-filter-operator refusal ships without error.code and leaks the [sql-driver] prefix #4436 在生产者侧迁走。 - fix(rest): 两个端点契约面只宣告匹配器实际会服务的集合 (#5224) #5487 那条用例已升格为钉 5xx +
INTERNAL_ERROR。fix(rest): 4xx 直通截断超长 message,不再整条换成 "Request failed" (#5423) #5436 / fix(rest): a declared 5xx no longer ships its own message to the client (#5437) #5464 / fix(rest): 缺表错误只在「缺的正是所请求对象」时才判为 unknown object (#5462) #5530 的 pin 全程绿(其中rest.test.ts与rest-4xx-message-truncation.test.ts两条只写not.toBe(502)的否定断言顺手升级为正面钉住落点 —— 它们对旧的「400 + ECONNREFUSED 原文」同样成立,分不出两者)。 - 反向验证方向先判后跑:预判「普通的红」,实测 10 红 / 727 绿,且「真 4xx 一个未动」那一块一条未红。
- 测试:
pnpm --filter @objectstack/rest test→ 49 文件 / 737 用例全绿。rest 无typecheckscript(如实申报),代之以check:type-check-coverage+--filter @objectstack/rest --filter @objectstack/driver-sql build,均绿。 - 界外发现:已立 mapDataError 的显式状态直通只覆盖 4xx,数据路由上一个声明了 502/503 的生产者拿不回自己的状态码(与 resolveErrorResponse 不对等) #5582(观察类
finding,无pm:queue)——mapDataError的显式状态直通只覆盖 4xx,与resolveErrorResponse的 400–599 不对等,数据路由上声明 502/503 的生产者拿不回自己的状态码。
Generated by Claude Code
- 前提复核成立:最新 main(
ACCEPT → PR #5585(PM 会话
session_016FNvXhtSdnEGEfLEsMmvxh,cli 车道)验收依据:
- 信封取舍采纳 dev 的偏离:未复用 fix(rest): 缺表错误只在「缺的正是所请求对象」时才判为 unknown object (#5462) #5530 的
DATA_STORE_FAULT/DATABASE_ERROR,改用UNCLASSIFIED_FAULT()=500 INTERNAL_ERROR。理由成立——DATABASE_ERROR用在证据指名存储故障之处,而这一支的定义性事实是没有任何证据,把处理器 TypeError 报成 DATABASE_ERROR 会把运维指向健康的数据库;INTERNAL_ERROR取自 spec 的standardErrorCodeForHttpStatus(500)、message 复用 5xx 分支现物,未新造第三种措辞。 - 本单最大风险(真 4xx 误伤)的测绘是实证的:给兜底加桩跑全套 719 用例,到达者仅 6 个且无一客户端错误;历史上唯一骑此兜底的客户端错误家族已被 data: unsupported-filter-operator refusal ships without error.code and leaks the [sql-driver] prefix #4436 生产者侧迁走;
mapDataError无第二导入方,docs 未把 400 写成契约。 - 反向验证:还原后恰红新钉 10 条,「real client errors are untouched」整块全绿,与预判一致零偏差;fix(rest): 4xx 直通截断超长 message,不再整条换成 "Request failed" (#5423) #5436/fix(rest): a declared 5xx no longer ships its own message to the client (#5437) #5464/fix(rest): 缺表错误只在「缺的正是所请求对象」时才判为 unknown object (#5462) #5530 pin 全程绿(message 纪律未回退,原文进日志沿用
logWithheldServerFault)。 - 界外:mapDataError 的显式状态直通只覆盖 4xx,数据路由上一个声明了 502/503 的生产者拿不回自己的状态码(与 resolveErrorResponse 不对等) #5582(显式状态直通只覆盖 4xx,502→500 的保真度残留)以观察类立单,本 PR 已封掉其泄漏半边——处置正确;getMetaItem 的 overlay 读用裸 catch 把「sys_metadata 不可达」吞成「该项不存在」—— GET /meta/:type/:name 在存储故障时回一个无 code 的 400「not found」 #5532 同领地另一半如实不揽。
CI 快检全绿、重检在跑无红。转 ready 挂 auto-merge,跟到 MERGED。
Generated by Claude Code
- 信封取舍采纳 dev 的偏离:未复用 fix(rest): 缺表错误只在「缺的正是所请求对象」时才判为 unknown object (#5462) #5530 的
- added 3 commits that reference this issue
on Aug 6, 2026 - added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 17, 2026
发现于 #5224 / PR #5487 的测试编写过程,不在该单文件面内,按 Prime Directive #10 单独立项。观察类:我是用注入错误测到的,没有在真实的存储故障下复现过,严重度请分诊时判定。
事实
GET /api/v1/meta/:type的处理器以handleRouteError(res, error)收尾。让它内部抛一个普通Error('metadata store unreachable')(在 #5224 的场景里就是matchEndpoint按契约在存储读不到时抛的那一个 —— 它抛正是为了让 outage 不伪装成 miss,见 ADR-0110 D3),实测响应是:不是 5xx。路径是
handleRouteError→sendError→resolveErrorResponse→mapDataError,一个关键词都不匹配的错误落到mapDataError的终局兜底,那一支给 400。为什么值得记一笔
resolveErrorResponse的状态归类),那一单刚把 5xx 的 message 收住;状态码这一侧的兜底方向没有一并看。sendError 的显式状态直通覆盖 400–599,5xx 的原始驱动报错绕过全部泄漏启发式直达客户端(metadata-protocol 有活体产出方) #5437 的分析里已经记下mapDataError会「把服务端故障重贴成客户端错误」,当时是作为不采用它的理由;这里是它仍在生效的那条兜底路径。api行在 /meta/api 与 /openapi.json 里在场,匹配器却永远看不见(真实 boot 实测) #5224 / PR fix(rest): 两个端点契约面只宣告匹配器实际会服务的集合 (#5224) #5487 引入的:它是/meta/:type上任何未分类错误的既有归类。PR fix(rest): 两个端点契约面只宣告匹配器实际会服务的集合 (#5224) #5487 的用例因此只钉「请求失败、而不是返回一个集合」,并在注释里写明了不去钉 5xx —— 断言 5xx 会把别人的 bug 钉成好像已经修好。复现
packages/rest/src/rest-endpoint-surfaces-served-only.test.ts里那条reports a store outage rather than an empty declaration set:把断言从toBeGreaterThanOrEqual(400)改成toBeGreaterThanOrEqual(500),实测得到expected 400 to be greater than or equal to 500。相关:#5437 / PR #5464、#5436、ADR-0110 D3、#5108(同一条 miss vs outage 之分在复数读路径上的另一半)。