Skip to content

/analytics/query 的请求体形状取决于装没装 service-analytics(降级 shim 收信封、真引擎收裸 AnalyticsQuery),且发错形状静默生成空 SELECT #3878

Description

@os-zhuang

按 Prime Directive #10 记录。修 #3867(PR #3875)时踩到的,是我自己被骗过去的那个坑 —— 它让我在 #3867 原文里得出了「没读到数据」的错误结论,所以值得单独收口。

同一个端点,两种请求体

POST /api/v1/analytics/query 的处理方由 analytics 服务槽决定,而这个槽有两个实现:

① 降级 shim(packages/metadata-protocol/src/plugin.ts)—— 收 {cube, query} 信封,并且明确做了两可兼容:

const envelope = body && typeof body === 'object' && 'query' in body && 'cube' in body
    ? body
    : { cube: body?.cube, query: body };
const result = await protocolShim.analyticsQuery(envelope);

② 真引擎(AnalyticsServicePlugin,ctx.replaceService 替换掉 ①)—— AnalyticsService.query(query: AnalyticsQuery, context?),收裸 AnalyticsQuery,即 {cube, measures, dimensions, filters, …} 全在顶层。没有信封兼容。

所以:

部署 {"cube":"x","measures":["count"]} {"cube":"x","query":{"measures":["count"]}}
未装 service-analytics(shim) ✅ 走 else 分支,被包成信封 ✅ 走 if 分支
装了 service-analytics(默认) ✅ ❌ 静默错误

同一个 URL、同一份 OpenAPI/discovery 描述,请求体契约却取决于部署里装了哪个插件。按 Prime Directive #12,这是「一个契约,N 种方言」;而且 ① 那个 'query' in body && 'cube' in body 的两可兼容本身就是 #12 说的「不要在消费端加宽容 fallback」的样本。

发错形状不报错,静默生成空 SELECT

这才是真正咬人的地方。真引擎收到带信封的 body 时,顶层没有 measures/dimensions,于是 ensureCube 推断出一个没有任何列的 cube,SQL 编译成:

POST /api/v1/analytics/query {"cube":"crm_account","query":{"measures":["count"]}}
→ 500 {"success":false,"error":{"message":"SELECT  FROM \"crm_account\" - near \"FROM\": syntax error","code":500}}

注意 SELECT 和 FROM 之间是空的。没有任何一层说「你的请求体形状不对」,它一路走到驱动才因为 SQL 语法炸掉。

我在 #3867 的第一次实证就是这么发的 payload,拿到语法错误,于是写下「只报了语法错误,没有读到数据,不宜当成数据泄露断言」。用正确形状重测才发现能读到真实行 —— 结论差了一个量级。一个静默接受畸形输入的端点会把调查者引向错误结论,这本身就是修它的理由。

(PR #3875 之后这条响应变成 Internal server error,SQL 不再回显;但「不报形状错误」这一点没变,只是更难诊断了 —— 这也是为什么应该在入口把它挡掉,而不是靠错误信息去猜。)

建议(未决策,留给维护者)

  1. 在入口用 Zod 校验请求体,以 AnalyticsQuery 为唯一契约,不符合就 400 并指出缺什么。AnalyticsQuery 在 @objectstack/spec/contracts 里已有类型;按 Prime Directive Add metamodel interfaces for ObjectQL/ObjectUI contract #1 应该有对应的 Zod schema 作为唯一真源。这一条独立于下面两条,单独做就能消除「静默空 SELECT」。
  2. 退役信封形状。按 Add comprehensive test suite for Zod schema validation #12 的做法:选 AnalyticsQuery 为规范形状,信封走 readAliasedConfig 一类的声明式 shim(warn 一次、可 lint、可按计划移除),而不是留一个裸 'query' in body 三元。
  3. 顺手确认 getMeta/generateSql 两侧签名是否也有同类分歧 —— shim 的 getMeta 忽略 cubeName 参数、generateSql 直接返回 {sql:null},与真引擎的行为差别是否已在 discovery 的 degraded 自述里如实反映。

倾向先做 1(小、独立、立刻消除静默失败),2 排在其后。

关联:#3867、PR #3875、#3770、ADR-0021、ADR-0076 D10/D12。

Activity

  1. os-zhuang commented on Jul 28, 2026

    @os-zhuang
    ContributorAuthor

    这个 shim 的出生证明 —— 它不是产品决策,是为了圆 discovery 的一句谎

    翻了一下来历。101d5c345(2026-05-16)把 fallback 加进 ObjectQLPlugin 时,注释自己写明了动机:

    Without this, HttpDispatcher's handleAnalytics cannot resolve a service and /api/v1/analytics/* returns ROUTE_NOT_FOUND, even though discovery advertises the route (objectql's getDiscovery hardcodes analytics: enabled:true).

    当时真正的缺陷是 discovery 硬编码 enabled: true。正确的修法是让 discovery 说实话;实际做的是反过来 —— 造一个实现,让那句谎话变成真的。11 天后 dacfe0fd2 又补上 'query' in body && 'cube' in body 那个两可三元,因为初版 query: (body) => protocolShim.analyticsQuery(body) 把裸 body 直接喂给解构 {query, cube} 的函数会当场炸。

    所以本 issue 说的"两种方言",是补丁的补丁。

    留着它的两条理由今天都已经失效

    1. "discovery 广告了这条路由" —— ADR-0076 D12 已经修好了(__serviceInfo → status: 'degraded')。谎话没了,圆谎的东西也就没有存在理由了。
    2. "minimal 部署不该 404" —— 代码里本来就是 404:槽位为空时 domains/analytics.ts:33 直接 return { handled: false }。这条路径一直存在且工作正常(plugin.step2.test.ts:50 还钉着"没有协议装配时 getService('analytics') 抛错")。shim 只是抢在它前面把槽位填了。

    ADR-0076 D10 说它 "duplicates logic only (a minor maintenance cost)" —— 这句已被实证推翻

    同一个安全门,因为有两个实现,修了两遍:

    一个洞、两次实现、两个 PR,中间还夹着一次严重性误判(#3867 原文因为发错形状而低估,也就是本 issue 的成因)。这不是 "minor maintenance cost"。

    补充一条本 issue 没提、但比空 SELECT 更咬人的分歧

    AnalyticsQuery 的规范过滤字段是 where(spec/data/analytics.zod.ts:154,"canonical Query DSL FilterCondition")。降级 shim 只读 query.filters(protocol.ts:3661)—— 而 filters 根本不是 AnalyticsQuery 的字段。也就是说:

    一个完全符合契约的带过滤请求,走到降级实现上,过滤条件被静默丢弃,返回的是全表聚合。

    objectui 的 dashboard 正是这么发的(data-objectstack:payload.where = params.filter)。空 SELECT 至少会炸;这个不炸,只是数字悄悄变大。

    (还有一条同源的、更严重的,涉及作用域,我单独开了 issue。)

    关于建议 1(入口 Zod 校验):现有 schema 不能直接拿来用

    AnalyticsQueryRequestSchema 确实已经在 spec/api/analytics.zod.ts:32 了,但它描述的是信封({query, cube, format});连 AnalyticsQuerySchema.cube 都是 optional 的,describe 里写着 "optional when provided externally, e.g. in API request wrapper"。规范里的"唯一真源"目前记录的是 shim 的方言,不是真引擎的形状。 所以落校验之前得先定哪个是规范形状,否则把现有 schema 接到入口上会直接把真引擎的正确请求判 400。

    建议

    退役 shim,让槽位空着,路由回到既有的 404。默认部署零影响:serve.ts:202 的 ALWAYS_ON_CAPABILITIES 已含 analytics(注释:"foundational post-ADR-0021"),真引擎照常装;托管环境也在 host 侧强制 analytics。shim 还在服务的只剩 --preset minimal、程序化 createStandaloneStack/createObjectQLKernel 嵌入、以及不强制该 capability 的宿主 —— 这些恰恰是应该 404 的场景:给出的信号从"数字悄悄错了"变成"你没装分析包"。

    本 issue 的建议 1 照做不误,两件事独立:真引擎同样需要在入口拒绝畸形请求,而不是一路走到驱动炸 SQL 语法。

    一并要处理的:objectql/src/plugin.step2.test.ts:39/41/65 钉住了 shim 的存在与 degraded 自述;http-dispatcher.test.ts 有一条"fallback 仍然广告 analytics 路由"的断言;cloud 的 service-ai-studio 只判 getService('analytics') 真假,会被 shim 骗过去;objectui 侧需要一条"未安装分析能力"的可读提示,而不是裸 404。

    以上代码位置核对于 origin/main @ 93f267f。

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions