fix(rest): 缺表错误只在「缺的正是所请求对象」时才判为 unknown object (#5462) - #5530
Conversation
…e object asked for (#5462) `mapDataError`'s unknown-object heuristic asked whether a driver error mentioned `no such table` / `relation ... does not exist` — never WHICH table was missing. A business object that was never registered and the metadata plane collapsing entirely are the same two words to that regex, so `sys_metadata` becoming unreachable came back as `404 {"error":"Object not found","code": "OBJECT_NOT_FOUND"}`: the caller was told to check the object name they typed. And 404 is an `isExpectedDataStatus`, so `handleRouteError` printed no "[REST] Unhandled error" — a total outage of the metadata plane left not one line in the server log. Reproduced in process on a real ObjectQL + ObjectStackProtocolImplementation whose driver fails every access with `SQLITE_ERROR: no such table: sys_metadata`: `PUT /api/v1/meta/object/acct` answered 404 with zero log lines. The rule now: a missing-relation message is an unknown-object verdict only when the relation it names is the object the request named. Attribution takes both halves — a request object, and a relation name the phrasing actually carries (schema qualifier stripped, compare case-insensitive). Prime Directive #6 (object name IS table name, no `tableName` mapping) is what makes the comparison sound rather than a guess. Anything unattributable is the sanitised data-store fault the SQL-leak branch has always emitted: 500 DATABASE_ERROR, which sits outside `isExpectedDataStatus` and so buys back the log line. Unchanged on purpose: a genuine unknown object is still a quiet 404 OBJECT_NOT_FOUND from both producers (#3770); the engine-authored limbs (`unknown object`, `no driver available`, the quoted-name catch-all) keep the old reading; and the declared-status band (#5437/#5464, #5423/#5436) answers in `resolveErrorResponse` before the heuristic is reached at all. 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
|
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
范围外发现(Prime Directive #10,已单独立卡、unassigned,不在本 PR 修):
同一族的错误结论(可用性故障讲成「不存在」),但根因在产出方、不在启发式:REST 层拿到手时真相已经被丢掉了,所以本 PR 的归属判定救不了它 —— 到 Generated by Claude Code Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31024070400 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes #5462
前提复核(先证后改)
按 rule 6,在 #5464 与 #5487 都已落地的
origin/main(308c70951)上先重跑复现,前提成立,与 issue 描述一字不差:真实
ObjectQL+ 真实ObjectStackProtocolImplementation,驱动每个方法都抛SQLITE_ERROR: no such table: sys_metadata。顺手把逃逸错误的形状也证了(PR 里保留为 premise guard 用例):status与code都没有 —— 这正是 #5437/#5464 的「显式 5xx」分支看不见它、而由文本启发式代为裁决的原因,也是本单必须单独修的理由。Postgres 措辞relation "sys_metadata" does not exist结果完全一致(同样 404 + 0 行日志)。判据形状的取舍
PM 给了两种可选形状。实测下来选了**归属判定(attribution)**这一种,理由写在这里以便复核:
SQLITE_ERROR/ 连接类)先行短路到基础设施分支」。实测这条会连坐:真实 unknown object 的 SQLite 报文同样带SQLITE_ERROR:前缀,短路后no such table: ghost也变 500,直接破坏 ②「真 404 不能变响」。name就是表名、没有tableName映射可洗,所以「缺的表 ≠ 你要的对象」确实等价于「故障不在你这个对象上」。新规则一句话:缺表类报文只有在它点名的 relation 就是本次请求点名的对象时,才判为 unknown object。 归属需要两半都成立 —— 有请求对象,且措辞里真的带得出 relation 名(
no such table: main.acct、relation "public.acct" does not exist,schema 前缀剥掉、大小写不敏感)。归属不成立的一律落已有的消毒故障信封
500 { "error": "Internal data error", "code": "DATABASE_ERROR" }—— 不是新造的第三种措辞,是 SQL-leak 分支一直在发的那一个,这次抽成一处以免两边漂移。500 不在isExpectedDataStatus里,被静默吞掉的那行日志就是这么找回来的;驱动原文仍然一个字都不到客户端。覆盖到的不可归属情形:换了一张表、辅助表、根本没有请求对象(metadata / UI / discovery 全家都走
handleRouteError(res, error)不带 object,正是本单复现的形状)、以及带不出 relation 名的措辞。刻意不动的三处
OBJECT_NOT_FOUND。 曝露 gate 对未知对象放行所依赖的「data path 会 404」并不成立 —— findData 无存在性校验(#3545 同类前提) #3770 的两个产出方仍落同一个信封(registry gate;以及缺的表就是所请求对象时的驱动分支),仍然一行日志都不打(Metadata routes log every expected 404 as "[REST] Unhandled error" with a stack trace — Studio's draft probe produced 45 in one browsing session #4886:对象不存在是客户端错误,不该刷栈)。unknown object、object not found、[ObjectQL] No driver available for object '< name >'(此处< name >的空格是为绕开 GitHub body 的 HTML 消毒器,代码里是紧挨的尖括号占位)、引号对象名兜底 —— 这些是我们自己关于某个具名对象的措辞,说什么就是什么。只有数据库撰写的那三条(它不可能知道调用方要的是哪张表)需要归属判定。resolveErrorResponse里先答,压根到不了启发式。⛔ 未动
served-endpoints.ts(#5487 刚落)。反向验证(方向先判后跑)
预判:还原旧启发式后,① 的误报 pin 应当红,②③ 保持绿 —— 普通方向,不是倒置族也不是「诊断变多」族,因为断言的是新故障判决本身。
实跑(把归属分支删掉、
looksLikeMissingRelation直接进looksLikeUnknownObject):方向如预判。一处需要如实说明:预判写的是「§1+§2 整段红」,实跑是 21 条里精确红掉 9 条 —— 剩下 3 条同段用例本来就该绿,不是漏网:premise guard 断言的是逃逸错误的形状(与本改动无关),另两条断言可归属的 relation 名(
main.acct/ 大小写)判 404,改动前后都是 404。这 3 条按「§1+§2 全红」的模板本该算失败,如实记在这里而不是去改用例迁就模板。还原后 ②③ 全绿,与预判一致。测试
新增
packages/rest/src/rest-unknown-object-heuristic.test.ts(21 条,四段:真实 harness 走完整条路 / 归属规则的单元边界 / 真 unknown object 仍静默 404 / #5464+#5436 不回归),复用 #5464 的 harness,in-process 用例一律}, 60_000)。合并
origin/main(4d573878a)后重跑同样 719 绿;进来的提交没有一个碰packages/rest,按 AGENTS.md §10 作范围化复核。pnpm typecheck这一项对本包不适用且我不打算伪造:@objectstack/rest没有typecheckscript,它在scripts/check-type-check-coverage.mjs的 DEBT + TEST_DEBT 台账里(#4311)。跑的是该台账自己的闸门,绿:外加
pnpm --filter @objectstack/rest build(含 DTS)通过,node scripts/check-nul-bytes.mjsOK。消费半径扫描
mapDataError只有packages/rest一个真消费方(其余仓内引用全是注释)。跨包只经 HTTP 集成用例暴露,逐一核过:packages/objectql的OBJECT_NOT_FOUND断言、packages/qa/dogfood/registry-gate-wiring、packages/runtime/domains/error-passthrough全部是 gate 产出的显式code+status,在启发式之上老远就被匹配走;packages/qa/http-conformance的 fixture 已syncObjectSchema('task'),其no such table只出现在讲历史的注释里,没有断言。Generated by Claude Code