Skip to content

REST 列表:无法解析的 filter JSON 被静默忽略 —— 返回未过滤整页(#4134/#4164 家族第三员) #4181

Closed

Description

@os-zhuang

TL;DR

GET /api/v1/data/<object>?filter={status:done(JSON 少个引号)—— findData 的解析容忍 try { JSON.parse } catch { /* keep as-is */ } 把原始字符串塞进 where,驱动层对字符串 where 静默忽略,返回未过滤的整页。

这是 #3948 那条原则(an unapplied filter must not look like a satisfied one)目前最恶性的破缺形态:#4134 是清零(少了),#4164 是丢一个谓词(多了一点),这里是整个过滤器蒸发(全都多了)。如果 filter 承担的是业务性收窄,多返回就是数据泄露的形状。

实测(真实 InMemoryDriver + 真 registry,3 行数据其中 1 行 status=done)

baseline (no filter)                     -> 200 total=3
filter='{"status":"done"}' (合法 JSON)    -> 200 total=1   ✅
filter='{status:done'      (垃圾字符串)   -> 200 total=3   ❌ 整页,无警告
filter='{status:done' + top=2            -> 200 total=3 records=2   ❌ 还在装模作样地分页

调用方拿到 total=3 和完全正常的信封,没有任何信号表明自己的 filter 从未生效。

病灶

packages/metadata-protocol/src/protocol.ts 的 filter alias 链:

if (typeof parsedFilter === 'string') {
    try { parsedFilter = JSON.parse(parsedFilter); } catch { /* keep as-is */ }
}
...
options.where = parsedFilter;   // 字符串原样成为 where

建议的方向:JSON.parse 失败即 400(INVALID_REQUEST 或注册一个专码,信封里带上解析失败的原文片段)。「宽容地保留原字符串」不是任何调用方需要的行为——没有驱动能消费字符串 where,这个容忍只制造静默。#4164 的 AND 合并已刻意绕开非 object 的 where(不往垃圾里合并谓词),并在测试里 pin 了这条路径的现状,等本 issue 在源头修掉。

顺带坐实的同链小弟

body 同时带 where 和 filter 时,options.filter ?? options.filters ?? options.$filter ?? options.where 让 filter 静默赢:

{ where: {status:'done'}, filter: {status:'open'} }   -> 200 total=2 (= status:open;where 被丢)

显式对显式的 drop。低频(body 是 Partial<QueryAST>,规范键是 where),但同病。修法可以跟主案一致:两者并存且不相等 → 400,或 AND 合并——留给实现者与 #4164 的语义对齐。

关联

#4134(未知参数清零,已修)、#4164(已知字段参数被显式 filter 挤掉,修复中)、#3948(原则出处)。三案修完建议补一个家族级 conformance:每一条 filter 表达路径,要么生效,要么抛错。

Activity

  1. os-zhuang commented on Jul 30, 2026

    @os-zhuang
    ContributorAuthor

    补一条会显著改变这个 issue 工作量的发现:处方已经存在,就在同一个文件的兄弟路由里。

    GET /data/:object/export 对同一个畸形输入早就是大声拒绝的 —— packages/rest/src/rest-server.ts:4730:

    let filter: any = undefined;
    if (typeof q.filter === 'string' && q.filter.length > 0) {
        try { filter = JSON.parse(q.filter); }
        catch {
            res.status(400).json({ code: 'INVALID_REQUEST', error: 'filter must be JSON' });
            return;
        }
    }

    所以这不是一个「要定语义」的问题,而是同一个端点家族里两条路由对同一份畸形输入给出相反答案:

    路由 ?filter={status:done(垃圾 JSON)
    GET /data/:object/export 400 INVALID_REQUEST — "filter must be JSON" ✅
    GET /data/:object(list) 200 + 未过滤整页 ❌

    而且方向是反直觉的:被硬化的是 export(一次导出,调用方看得见结果),没硬化的是 list(常被程序化消费,多返回最不容易被发现)。

    实现建议就是把这段守卫搬到归一化层(protocol.ts 的 filter alias 链),让两条路由共用一个答案。错误码直接复用 INVALID_REQUEST —— 它已在 @objectstack/rest 的 ledger 里注册,不需要新码。搬到 protocol 层而不是 list 路由,顺带覆盖 POST /data/:object/query 与 runtime dispatcher 那条路径。

    同一段代码里的小弟(同病,低危)

    紧邻的 orderby 解析(rest-server.ts:4745)在 JSON.parse 失败时静默 undefined:

    try { orderby = JSON.parse(q.orderby); } catch { /* leave undefined */ }

    排序被丢弃不改变结果集(仍是诚实的全集,只是顺序不是要的那个),所以危害远低于 filter;但如果调用方靠 orderby + top 取「最新 N 条」,拿到的就是任意 N 条。修 filter 时顺手一起处理比较合算。


    Generated by Claude Code

  2. self-assigned this
    on Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions