fix(rest): a declared 5xx no longer ships its own message to the client (#5437) - #5464
Merged
Merged
Conversation
…nt (#5437) `resolveErrorResponse` — the value side of `sendError`, and therefore the error path of every metadata / UI / discovery / batch route — passed an explicit status straight through for the whole 400-599 band. A declared 5xx returned `error.message` verbatim, past `isSqlLeak`, past `looksLikeInternalErrorLeak`, past the `Internal data error` envelope, while `mapDataError`'s sibling branch stops at 4xx on purpose and says why: "5xx messages keep going through the sanitizing heuristics below so internal/SQL details never reach the client verbatim". Two opposite verdicts on one question. `metadata-protocol` interpolates the raw driver error into two client-facing 500s (overlay persist / delete), and a real driver line is far shorter than the 500-character bound that was the only thing standing here, so the whole thing arrived intact. Length was never a proxy for leakage; on this side of the bound it failed open. The 5xx band now drops the message unconditionally and keeps the producer's status and `code`. Unconditional rather than heuristic: a keyword gate only moves the question to "does the predicate know this dialect". Sanitised in the branch rather than by falling through to `mapDataError`, which derives status from message TEXT — measured first, and it answers 404 OBJECT_NOT_FOUND for the overlay 500s, 404 "Object '<name>' is not registered" for the atomic batch's 501, and 400 with the driver text still verbatim for anything its keywords miss. The withheld text still reaches the log: `handleRouteError` already prints a genuine fault, and a new line covers the 502/503 gap its predicate leaves. 4xx truncation (#5423 / #5436) is untouched. 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 1 package(s): 11 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 13:07
baozhoutao
enabled auto-merge
August 5, 2026 13:07
This was referenced Aug 5, 2026
Closed
This was referenced Aug 6, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…e object asked for (objectstack-ai#5462) (objectstack-ai#5530) `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 objectstack-ai#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 (objectstack-ai#3770); the engine-authored limbs (`unknown object`, `no driver available`, the quoted-name catch-all) keep the old reading; and the declared-status band (objectstack-ai#5437/objectstack-ai#5464, objectstack-ai#5423/objectstack-ai#5436) answers in `resolveErrorResponse` before the heuristic is reached at all. Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…a missing item (objectstack-ai#5532) (objectstack-ai#5705) The customization-overlay reads in `getMetaItems`/`getMetaItem` each wrapped their sys_metadata access in a bare `catch {}` and answered with their own empty value, so a metadata store the protocol could not reach was indistinguishable from an item nobody ever customised. The emptiness then travelled the read chain and each consumer named it differently and wrongly: `getMetaItemCached` as `Metadata item <type>/<name> not found`, the `state='draft'` read as `NO_DRAFT`/404, `getMetaItems` as `items: []`. ADR-0110 D3: a miss and an outage are different facts with opposite meanings. objectstack-ai#5108 fixed this in DatabaseLoader's plural read and objectstack-ai#5089 in listForIndex; this is the same rule on the protocol's own overlay reads. Discrimination is by error TYPE through `isMissingTableError` — the predicate DatabaseLoader (objectstack-ai#5108) and SysMetadataRepository (objectstack-ai#4867) already ask, so a driver quirk is taught to the platform once. The one benign reason (the table is not provisioned yet) still falls through to the registry; everything else throws 503 + SERVICE_UNAVAILABLE with the driver error as `cause`, which the REST boundary's existing objectstack-ai#5437/objectstack-ai#5464 sanitising and logging already handle. The terminal miss in `getMetaItemCached` is structured too: 404 + RESOURCE_NOT_FOUND, so a plain miss stops falling out of `mapDataError`'s catch-all as an unattributable 500 (and, pre-objectstack-ai#5489, as a 400 shipping the internal wording verbatim). Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
… (objectstack-ai#5774) * fix(runtime): unknown /auth sub-paths get a clean 404 instead of a leaked internal TypeError The dispatcher plugin mounted one legacy explicit route, POST ${prefix}/auth/login, which handed better-auth the adapter's INTERNAL IHttpRequest (headers is a plain object, not Headers). better-auth's fetch-style handler opens with request.headers.get(...), so the route answered HTTP 500 with the raw 'request.headers.get is not a function' in the response body. /login is not a better-auth endpoint at all, so the mount could never work for any caller. - dispatcher-plugin.ts: delete the legacy route. Unknown auth sub-paths now fall to the /auth/* wildcard the namespace owner mounts on the raw Hono app, which forwards a real Fetch Request and yields better-auth's own clean 404. - domains/auth.ts: a throw out of IAuthService.handleRequest is unattributable here, so its message is withheld unconditionally (objectstack-ai#5437/objectstack-ai#5464/objectstack-ai#5489 discipline) — 500 INTERNAL_ERROR with the original error on the server log. Refs objectstack-ai#5085 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWUR56YsttL5sTF72Q75TQ * test(runtime): pin both halves of the objectstack-ai#5085 /auth forwarding fix + changeset - auth-forward-fault-sanitization.test.ts: the dispatcher plugin mounts NO route under ${prefix}/auth (the specific legacy mount AND the general invariant), and a throw out of IAuthService.handleRequest is a sanitised 500 whose body carries none of the thrown text, with the original error on the server log. Positive controls: a better-auth Response passes through untouched (same object), its own 401 body is not sanitised, and an empty auth slot still 501s. - auth-unknown-subpath.hono.integration.test.ts: a real hono boot, with a fake auth service that is better-auth-SHAPED (reads request.headers.get + new URL(request.url) first thing, so an internal request object explodes here the way it did in production). POST /auth/login → 404, sign-in/email → 200 with its set-cookie. Refs objectstack-ai#5085 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWUR56YsttL5sTF72Q75TQ --------- Co-authored-by: Claude <noreply@anthropic.com>
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 #5437
前提核验(在
origin/main01c0baef上)issue 的成因描述仍然成立,行号已漂移:直通区间 400–599 在
packages/rest/src/rest-server.ts:831-832;两个活体产出方在packages/metadata-protocol/src/protocol.ts:7462-7467(OVERLAY_PERSISTENCE_FAILED,status = 500字面量)与:9813-9814(Failed to delete customization overlay,status动态赋值);第三个同区间产出方:5582(status = 501,NOT_IMPLEMENTED)也已核对。落地方向:A,但消毒点在分支内,不是「落回
mapDataError」分诊裁定的是方向 A(收窄到 4xx、5xx 走消毒)。实现前先把「让 5xx 落回
mapDataError」这条字面路径实测了一遍,结论是它做不到裁定要的结果,因此消毒改在直通分支内完成,状态与code原样保留。实测输出(用真实 fixture 直接调mapDataError):mapDataError的答复...sys_metadata: SQLITE_ERROR: no such table: sys_metadata404 OBJECT_NOT_FOUND/Object not found...: relation "sys_metadata" does not exist404 OBJECT_NOT_FOUNDFailed to delete customization overlay: connect ECONNREFUSED 10.0.0.5:5432400,驱动原文逐字保留Atomic batch on 'showcase_account' requires ...(带 object 参数)404 Object 'showcase_account' is not registeredSearch index rebuild is in progress; retry in 30s.400,原文逐字三个问题:①
mapDataError是按消息文本推导状态的,服务端故障被改写成客户端错误,能力拒绝被改写成「对象不存在」;② 改写后的 404 落在isExpectedDataStatus里,这条故障连日志都不打了;③ 关键词一条都不命中的 5xx 落到mapDataError的兜底{ status: 400, error: raw }—— 原文照样出去,只是换了个 4xx 的壳。第 ③ 条正是分诊否决 B 时写的「判准必然漏」,只是漏在兜底而不是漏在启发式。分诊要求的 pin ③(501 家族同验)恰好就是证伪这条路径的那一条。所以 5xx 的处置是:保留产出方声明的状态、保留
code、丢掉正文,正文换成@objectstack/types的INTERNAL_ERROR_MESSAGE。无条件丢弃而不是过启发式 —— 加判准只是把问题挪成「这个方言启发式认不认识」,而这正是本 bug 的成因。⛔ 未动:
isSqlLeak/looksLikeInternalErrorLeak本身、mapDataError、#5436 的 4xx 截断。文件面只有packages/rest。接受代价(已按分诊口径写进 changeset)
自撰 5xx 正文客户端将读到通用文案 + 原
code:OVERLAY_PERSISTENCE_FAILED的「In-memory registry was updated but will be lost on restart」、原子批的「retry without options.atomic」都属此类。code仍然随响应下发(SCREAMING_SNAKE 常量不是泄漏,也正是客户端该 key 的东西),按 code 判断的客户端不受影响。日志侧补齐:
sendError原先完全不打日志,5xx 一旦消毒就会从「客户端读得到」直接变成「没人读得到」。新增logWithheldServerFault,只在「正文确实被扣下」时打一行;handleRouteError对真故障已经打整个 error 对象,所以这行只补它判定为「预期」而静默的 502/503 那个口子 —— 一次故障恰好一行,不重复(有专门用例钉住)。补齐 issue 正文的「未验证部分」
ObjectQL+ 真实ObjectStackProtocolImplementation+ 每个方法都抛驱动错的 mock 驱动,打真实路由DELETE /api/v1/meta/:type/:name,走出真实的Failed to delete customization overlay: SQLITE_ERROR: no such table: sys_metadata,断言客户端 body 里没有任何一段原文、状态仍是 500、原文完整进日志。SQLite 与 Postgres 两种措辞各一条。(e as any).status = 500那个产出方 ——status:字面量 grep 找不到它,正是 issue 说没清点的形状。方法记录在测试文件顶部。saveMetaItem的OVERLAY_PERSISTENCE_FAILED未做 live:走到它的 legacy 分支要求「既不可 overlay 又不可 runtime create」的类型且该项是 artifact-backed,运行时新建会先被403 NOT_CREATABLE拦下。改为按产出方原样构造消息形状覆盖,测试里写明了这一点,没有假装走通。反向验证(方向先判后跑)
预判:把区间改回
< 600(恢复「短于 500 字符逐字、超长换 Request failed」)应当恰好红掉 5xx 消毒 pin,4xx 截断 pin 全绿。实跑一致 —— 10 红 14 绿,红的全部是 5xx 处置断言(含 499/500 边界那条,它按设计依赖 5xx 侧),#5423/#5436 的每一条 4xx 截断用例保持绿。一条例外值得写明:「一次故障恰好一行日志」那条在还原后仍然绿。它不是修复 pin,而是防重复日志的守卫 —— 还原后正文照常直通,
logWithheldServerFault自然 no-op,只剩handleRouteError那一行。这条断言的方向就是「无论哪一侧都只有一行」,不是「改前后不同」。fixture 处置
rest-4xx-message-truncation.test.ts里那条「an over-long 5xx is DELIBERATELY still replaced」精确地钉住了本 PR 删掉的那条限枝(它断言的'Request failed'恰好来自被删的长度分支),按整体替换处理:重写为钉新的处置(不论长度一律扣下、code保留),并指向新文件。#5423 的其余断言一字未动。消费半径已扫:
resolveErrorResponse是rest-server.ts私有函数(对外只导出mapDataError),依赖@objectstack/rest的 5 个包中没有任何测试驱动RestServer,runtime / services 的 envelope conformance 走的是另一条边界(errorResponseBase)。验证
pnpm --filter @objectstack/rest test→ 45 files / 676 tests passedpnpm --filter @objectstack/rest build(tsup DTS 即类型检查)→ success;node scripts/check-type-check-coverage.mjs→ OK(@objectstack/rest在 DEBT 账本内,无typecheck脚本,未新增误差)node scripts/check-nul-bytes.mjs→ OK,另对改动文件做了控制字符自扫顺带记录
方向 C(产出方不插值驱动原文)按分诊不在本单。落地后另有一条本单实测撞见的旁证已单独立项:#5462 ——
sys_metadata整体不可用时,不带status的原始驱动错误被looksLikeUnknownObject误报成404 OBJECT_NOT_FOUND,且 404 属「预期状态」因而一行日志都不留。那条不带显式 status,不受本 PR 影响。🤖 Generated with Claude Code
https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
Generated by Claude Code