Skip to content

fix(rest): 4xx 直通截断超长 message,不再整条换成 "Request failed" (#5423) - #5436

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5423-rest-4xx-truncate-not-replace
Aug 5, 2026
Merged

fix(rest): 4xx 直通截断超长 message,不再整条换成 "Request failed" (#5423)#5436
baozhoutao merged 1 commit into
mainfrom
claude/issue-5423-rest-4xx-truncate-not-replace

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5423

按分诊裁定取 B(截断而非替换)

前提复核(对 origin/main = 123067ce7)

issue 的两处行号原样成立,未发生漂移:

  • packages/rest/src/rest-server.ts:566-569mapDataError 的 4xx 直通,length < 500 二分。
  • packages/rest/src/rest-server.ts:785-790resolveErrorResponse(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.ts unsupportedFilterError 的注释 —— 仅注释,零行为变更。原文说 status: 400sendError「pass the message through instead of routing it to the SQL-leak heuristic」,与实测相反:不带 status 时原文本就经 mapDataError 末尾的 raw 分支完整直达(泄漏启发式不命中这些措辞),带上 status 反而进了这道闸门。

补齐 issue 的「未验证部分」

  1. sendError 分支实际走通了,不是按同款推断:用该文件既有的 in-process harness(RestServer + mock server + getRoutes() 取 handler),让 PUT /api/v1/meta/:type/:name 真实抛错,读 res.json 收到的 body。
  2. 非文件过滤器类 4xx 抽查:metadata save 的 422(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 的注释里。
  3. 结构化半边(issues[]codestatus)全程不受影响,有断言。

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

预判:把 slice 改回整条替换,应当恰好 7 条红 —— mapDataError 3 条长消息断言 + 1 条边界(499 逐字 / 500 首次截断)+ sendError 2 条 + 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)。

验证

changeset:@objectstack/rest patch。


🤖 Generated with Claude Code

https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh


Generated by Claude Code

…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
@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 11:49am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/rest.

18 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/data-modeling/drivers.mdx (via @objectstack/driver-sql)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-sql)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-sql, @objectstack/rest)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-sql, @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.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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

队列构建 31003481598 红了。队列跑的是全量套件(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 队列共有 2 个失败构建(不含本次)。

分诊清单:

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

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

Merged via the queue into main with commit 39396bd Aug 5, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5423-rest-4xx-truncate-not-replace branch August 5, 2026 12:03
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…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>
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>
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/m tests tooling

Projects

None yet

2 participants