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)

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