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 又新增了一条。逐条量它们的字面量长度(9c5abf4e9 的 sql-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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions