Skip to content

fix(rest): 缺表错误只在「缺的正是所请求对象」时才判为 unknown object (#5462) - #5530

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-5462-unknown-object-heuristic
Aug 5, 2026
Merged

fix(rest): 缺表错误只在「缺的正是所请求对象」时才判为 unknown object (#5462)#5530
baozhoutao merged 2 commits into
mainfrom
claude/issue-5462-unknown-object-heuristic

Conversation

@baozhoutao

@baozhoutao baozhoutao commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #5462

前提复核(先证后改)

按 rule 6,在 #5464#5487 都已落地的 origin/main308c70951)上先重跑复现,前提成立,与 issue 描述一字不差:

PUT /api/v1/meta/object/acct   =>  404 {"error":"Object not found","code":"OBJECT_NOT_FOUND"}
                                   日志行数 0
DELETE /api/v1/meta/object/acct => 500 {"error":"Internal server error"}   ← #5464 已修好的那一侧

真实 ObjectQL + 真实 ObjectStackProtocolImplementation,驱动每个方法都抛 SQLITE_ERROR: no such table: sys_metadata。顺手把逃逸错误的形状也证了(PR 里保留为 premise guard 用例):

ASYNC-THROW saveMetaItem status=undefined code=undefined name=Error
            msg=SQLITE_ERROR: no such table: sys_metadata

statuscode 都没有 —— 这正是 #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 不能变响」。
  • 选了「缺的那张表,是不是调用方点名的那个对象」。这是 issue 自己指出的语义差(「不区分缺的是哪张表」),并且判据不是又一层措辞猜测:Prime Directive Migrate from npm to pnpm #6 规定对象 name 就是表名、没有 tableName 映射可洗,所以「缺的表 ≠ 你要的对象」确实等价于「故障不在你这个对象上」。

新规则一句话:缺表类报文只有在它点名的 relation 就是本次请求点名的对象时,才判为 unknown object。 归属需要两半都成立 —— 有请求对象,且措辞里真的带得出 relation 名(no such table: main.acctrelation "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 名的措辞。

刻意不动的三处

⛔ 未动 served-endpoints.ts#5487 刚落)。

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

预判:还原旧启发式后,① 的误报 pin 应当,②③ 保持绿 —— 普通方向,不是倒置族也不是「诊断变多」族,因为断言的是新故障判决本身。

实跑(把归属分支删掉、looksLikeMissingRelation 直接进 looksLikeUnknownObject):

❯ src/rest-unknown-object-heuristic.test.ts (21 tests | 9 failed)
  × PUT /meta/object/acct answers 5xx, not 404 OBJECT_NOT_FOUND
      AssertionError: expected 404 to be 500
  × the fault leaves exactly one log line, carrying the driver text
      AssertionError: expected +0 to be 1          ← 0 行日志,issue 描述的静默
  × the driver text still does not reach the client
      expected { error: 'Object not found', ...(2) } to deeply equal
               { error: 'Internal data error', ...(1) }
  × Postgres phrasing lands identically
  × no request object at all → fault
  × a DIFFERENT table than the requested object → fault
  × an auxiliary table of the requested object is still a fault, not a 404
  × an empty-string object cannot be attributed to → fault
  × a phrasing that names no relation cannot be attributed to → fault
  Tests  9 failed | 12 passed (21)

方向如预判。一处需要如实说明:预判写的是「§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)

pnpm --workspace-concurrency=2 --filter @objectstack/rest test -- --maxWorkers=2
  Test Files  48 passed (48)
       Tests  719 passed (719)

合并 origin/main4d573878a)后重跑同样 719 绿;进来的提交没有一个碰 packages/rest,按 AGENTS.md §10 作范围化复核。

pnpm typecheck 这一项对本包不适用且我不打算伪造@objectstack/rest 没有 typecheck script,它在 scripts/check-type-check-coverage.mjs 的 DEBT + TEST_DEBT 台账里(#4311)。跑的是该台账自己的闸门,绿:

check-type-check-coverage: OK — 62/77 workspace packages type-checked (plus the root),
  15 in the DEBT ledger (358 frozen raw errors), 1 exempt.

外加 pnpm --filter @objectstack/rest build(含 DTS)通过,node scripts/check-nul-bytes.mjs OK。

消费半径扫描

mapDataError 只有 packages/rest 一个真消费方(其余仓内引用全是注释)。跨包只经 HTTP 集成用例暴露,逐一核过:packages/objectqlOBJECT_NOT_FOUND 断言、packages/qa/dogfood/registry-gate-wiringpackages/runtime/domains/error-passthrough 全部是 gate 产出的显式 code+status,在启发式之上老远就被匹配走;packages/qa/http-conformance 的 fixture 已 syncObjectSchema('task'),其 no such table 只出现在讲历史的注释里,没有断言。


Generated by Claude Code

claude added 2 commits August 5, 2026 15:37
…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
@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 3:43pm

Request Review

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

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest.

11 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/permissions/authentication.mdx (via @objectstack/rest)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)
  • content/docs/releases/implementation-status.mdx (via @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.

Copy link
Copy Markdown
Contributor Author

范围外发现(Prime Directive #10,已单独立卡、unassigned,在本 PR 修):

同一族的错误结论(可用性故障讲成「不存在」),但根因在产出方、不在启发式:REST 层拿到手时真相已经被丢掉了,所以本 PR 的归属判定救不了它 —— 到 mapDataError 的那条报文里既没有 no such table 也没有任何 relation 名可归属。查重已做(关键词 + sys_metadata / getMetaItem / 文件路径,含已关闭):最接近的 #5108DatabaseLoader复数读路径且已修,其标题限定语即为此,不构成重复。


Generated by Claude Code


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review August 5, 2026 15:55
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 90fa077 Aug 5, 2026
25 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5462-unknown-object-heuristic branch August 5, 2026 16:22
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31024070400 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run the non-SQL temporal backends under the skewed process zone

    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mretires a legacy global unique index and replaces it with the composite
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mretires the legacy `uniq_<table>_<col>` index left by the drift rebuild path
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2ma
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2mB
    �[22m�[39m[schema-drift] product: cannot tighten 'uniq_product_organization_id_code' as UNIQUE (COALESCE(organization_id, '__global__'), code) — existing rows already violate the NULL-safe unique cons
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2mB
    �[22m�[39m[schema-drift] REFUSING to rebuild 'uniq_product_organization_id_code' on 'product' as a NULL-safe unique — 1 duplicate group(s) violate it (e.g. organization_id="__global__", code="DUP" × 2
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 14 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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/l tests tooling

Projects

None yet

2 participants