Uh oh!
There was an error while loading. Please reload this page.
fix(rest): 4xx 直通截断超长 message,不再整条换成 "Request failed" (#5423) - #5436
Conversation
…erasing it (#5423) Both explicit-status passthrough branches in `rest-server.ts` bounded a domain error's message at 500 characters by REPLACING it with the literal 'Request failed' — `status` and `code` landed as usual and every word of the body text disappeared. That inverted the incentive on the whole rejection vocabulary. driver-sql's filter refusals exist only to tell an author which operator or field they got wrong and how the spec declares it, and the two most carefully worded of them (#5158's unlowered FilterArray, #5347's non-boolean $null comparand) are both over the bound — so the more precisely a rejection was written, the more certainly the client read nothing. They were also readable BEFORE they carried a status, through `mapDataError`'s final raw-message fallback: #4436 added `status: 400` to give them an ADR-0112 wire identity and, in this band, cost them their body. An over-long message is now truncated to `slice(0, 499) + '…'` — same shape as the drivers' own `safeShapePreview`. These messages front-load the main clause (operator, field, path, what arrived, what the spec declares) and back-load attribution and issue numbers, which belong in the log. The bound stays at 500; what changed is what happens AT it. Messages under it are byte-for-byte unchanged. `resolveErrorResponse`'s passthrough range is 400-599, wider than `mapDataError`'s; only its 4xx half changes. 5xx keeps the wholesale replacement, matching the sibling branch's recorded "deliberately limited to 4xx ... so internal/SQL details never reach the client verbatim". Also corrects `sql-driver.ts`'s `unsupportedFilterError` docblock, which claimed `status: 400` "makes sendError pass the message through instead of routing it to the SQL-leak heuristic" — the opposite of the measured behaviour. Comment only; no driver behaviour change. 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 2 package(s): 18 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31003481598 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
…nt (objectstack-ai#5437) (objectstack-ai#5464) `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 (objectstack-ai#5423 / objectstack-ai#5436) is untouched. Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh Co-authored-by: Claude <noreply@anthropic.com>
…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>
Fixes#5423
按分诊裁定取 B(截断而非替换)。
前提复核(对
origin/main=123067ce7)issue 的两处行号原样成立,未发生漂移:
packages/rest/src/rest-server.ts:566-569—mapDataError的 4xx 直通,length < 500二分。packages/rest/src/rest-server.ts:785-790—resolveErrorResponse(sendError的取值端),同款二分。一处需要更正 issue 的表述:
sendError那处的直通区间是 400–599,不是 4xx(error.status >= 400 && error.status < 600)。这一点影响了实现取舍,见下。改了什么
抽出一个
truncateClientMessage(),超长时slice(0, 499) + '…',与驱动侧safeShapePreview同源。上限仍是 500,变的是到达上限时的处理方式;短于 500 的消息逐字不变(空/缺失 message 仍降级为Request failed—— 无可截断)。sendError一侧只改 4xx 那一半,5xx 的整条替换逐字保留。理由是这个 4xx/5xx 之分是仓库既有的既定取向,mapDataError同族分支的注释已经写死:「deliberately limited to 4xx: 5xx messages keep going through the sanitizing heuristics ... so internal/SQL details never reach the client verbatim」。4xx 的正文是写给调用方的补救说明;5xx 的正文是服务端故障的日志诊断,恰好在这里够得着而已。放宽 5xx 不在本单范围内,也不该作为搭车项落地。另按裁定改真
packages/plugins/driver-sql/src/sql-driver.tsunsupportedFilterError的注释 —— 仅注释,零行为变更。原文说status: 400让sendError「pass the message through instead of routing it to the SQL-leak heuristic」,与实测相反:不带 status 时原文本就经mapDataError末尾的 raw 分支完整直达(泄漏启发式不命中这些措辞),带上 status 反而进了这道闸门。补齐 issue 的「未验证部分」
sendError分支实际走通了,不是按同款推断:用该文件既有的 in-process harness(RestServer+ mock server +getRoutes()取 handler),让PUT /api/v1/meta/:type/:name真实抛错,读res.json收到的 body。INVALID_METADATA)。顺带一个值得记的实测:按metadata-protocol的真实构造(前 3 条 issue 摘要 +(+N more)),三条 issue 的版本量到 492 字符 —— 距离越线只差 8 个字符。metadata save 不是冷门路径、五条校验错误也不是冷门错误,所以这一族和 issue 猜测的那两条临界过滤器拒收({ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 ~469、fix(driver-sql,driver-memory,formula)!:{ field: {} }四个后端一律拒收 —— 零个操作符的字段约束不再有三个答案 (#5240) #5327 ~454)一样,是骑在悬崖边上的。这条测量写进了 fixture 的注释里。issues[]、code、status)全程不受影响,有断言。反向验证(方向先判后跑)
预判:把
slice改回整条替换,应当恰好 7 条红 ——mapDataError3 条长消息断言 + 1 条边界(499 逐字 / 500 首次截断)+sendError2 条 +rest.test.ts那条既有 fixture;而全部短消息 / 空消息 / 5xx 断言应当保持绿(它们防的是反向过度修改)。实跑:
Tests 7 failed | 656 passed,失败集与预判逐条一致。Fixture 处置
rest.test.ts:2325既有的guards the passthrough message length (oversized → generic text)正是钉住我要改的那条限的 fixture —— 它把「整条替换」当作意图钉了下来,是这个行为能一直安静存在的原因之一。按整条替换处置:保留它守卫的边界(超长不得整条到达客户端),改为断言截断而非抹除,并注明完整覆盖在新文件里。除此之外全仓无第二处消费方依赖这条限(已 grep)。验证
pnpm --filter @objectstack/rest --filter @objectstack/driver-sql test→ rest44 files / 663 tests全绿;driver-sql63 passed | 4 skipped / 855 passed全绿。pnpm --filter @objectstack/driver-sql typecheck→ Done。@objectstack/rest没有typecheck脚本 —— 它在scripts/check-type-check-coverage.mjs的 DEBT + TEST_DEBT 台账里([P2] framework: 66 个包用 tsup 构建、无人做类型检查 —— 实测 18 个包共 380 处 code-tier 错误(#4118 的 framework 侧对应) #4311),是既有状态,非本次引入。跑了台账本身:check:type-check-coverageOK,62/77 覆盖,未松动。node scripts/check-nul-bytes.mjsOK;改动文件另做了grep -naP控制字符自扫,clean。changeset:
@objectstack/restpatch。🤖 Generated with Claude Code
https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
Generated by Claude Code