Skip to content

rest-server 的 4xx 直通把 ≥500 字符的 message 整条换成 "Request failed" —— #5368 刚写好的过滤器拒收措辞,客户端一个字也收不到(实测) #5423

Description

@os-zhuang

做 cloud#1116(把 Turso RemoteTransport 的过滤器拒收搬进 ADR-0112 信封,与 #5368 对齐)时扫到。不在那个 PR 范围内(跨仓),按 Prime Directive #10 单独记在这里,unassigned。

成因

packages/rest/src/rest-server.ts 有两处同样的 4xx 直通分支,都按 500 字符做二分:

mapDataError(:566-569)

if (typeof error?.status === 'number' && error.status >= 400 && error.status < 500) {
    const msg = typeof error?.message === 'string' && error.message.length > 0 && error.message.length < 500
        ? error.message
        : 'Request failed';

sendError(:785-790)

const safeMsg = typeof error.message === 'string' && error.message.length < 500
    ? error.message
    : 'Request failed';

超过 500 字符不是截断,是整条替换code / status 照常落地,正文全部消失。

为什么现在是 bug

driver-sql 的过滤器拒收自 #4436 起就带 status: 400#5368 又新增了一条。逐条量它们的字面量长度(9c5abf4e9sql-driver.ts${…} 按 4 字符占位,即低估):

# 拒收
1 A filter ARRAY reached the driver…#5158 503
7 Operator "$null" … requires a boolean comparand…#5347/#5368 585
5 Field constraint at … carries zero operators#5240 469
6 Unsupported filter combinator …#5327 454
2 跨字段比较 349
4 Filter node at … not a filter condition#5134 402

第 1、7 两条已经越线;第 5、6 条差 30-50 字符,而 safeShapePreview 单独就能贴进 80 字符、字段名和 path 还要再加——真实调用里同样越线。

所以 #5347 那条精心写出来的句子——「注意 "false" 这个字符串是 truthy,所以它落到了它本意的反面」——REST 客户端拿到的是:

{ "code": "INVALID_FILTER", "error": "Request failed" }

写这条 message 的唯一目的就是告诉作者他写错在哪,而它恰好因为写得够详细而被丢掉。

unsupportedFilterError 自己的注释还把这件事讲反了(sql-driver.ts :456):

status: 400 makes @objectstack/rest's sendError pass the message through instead of routing it to the SQL-leak heuristic

实测:不带 status 时走的是函数底部 return { status: 400, body: { error: raw } },原文完整直通(looksLikeInternalErrorLeak 不命中这些措辞);带上 status: 400 反而进了这个 500 字符闸门。也就是说在 ≥500 字符这一档,加 status 让客户端可读性变差了——这显然不是 #4436 的本意。

波及面

不止过滤器:任何带 4xx status 的领域错误都吃这一刀(plugin-sharing 的 record-scope 拒绝、metadata save validator 的 422 等)。凡是「写得越清楚越容易被吞」的错误都在这个集合里。

建议(不代裁决)

倾向 B

  • A:把上限调高(比如 2000)。一行改完,但没解决「二分」本身——只是把悬崖挪远一点,下一条写长的 message 照样掉下去,而且掉下去的方式仍然是静默整条替换
  • B(推荐):截断而不是替换 —— error.message.slice(0, N) + '…',与驱动侧 safeShapePreview / cloud preview() 已有的做法同源。前 N 个字符恰好是每条 message 的主句(操作符、字段、收到了什么、协议怎么声明的),被砍掉的是尾部的归因与 issue 号——正好是日志该留、客户端不必读的部分。这条闸门的本意是防 SQL/内部细节泄漏,而这些 message 已经过了 looksLikeInternalErrorLeak;长度从来不是泄漏的代理指标。
  • C:什么都不改,改短 message。把压力推给每一个驱动作者,且是不可执行的约定——没有任何 gate 量长度,越线是静默的。反方向。

B 的话建议同时补一条 pin:给一个 600 字符的 4xx 错误跑 mapDataError,断言正文的主句还在。今天没有任何测试观察这个分支的长 message 一侧,这也是它能安静地存在到现在的原因。

未验证的部分

按代码读 + 逐条量长度得出,没有起 REST server 打真实请求。两处分支的判据一模一样,但只核过 mapDataError 这一处的上下游;sendError 那处(:785)是同款写法,按同款处理,未单独走通。也没清点非过滤器类 4xx 里实际有多少条越线。

关联

#4436(过滤器拒收进信封的那一单)、#5368 / #5347(本次触发排查的 $null 措辞)、#5158 / #5240 / #5327 / #5134(其余越线或接近越线的拒收)、#5367(同族:analytics 路由靠 message 正则分类)、cloud#1116(下游把同一批拒收搬进同一个信封,会吃同一刀)。


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions