Skip to content

GET /openapi.json 有两个属主:rest-server 真serve,http-dispatchergenerateOpenApi 分支全仓无实现(ADR-0076 D1 影子重复) #5078

Description

@os-zhuang

#4936 / #4939 的实施(PR #5065)中,摘除 handleApiEndpoint 死分支时,在紧邻的上一段代码里发现同一形状的第二处,记录备查,未认领。

基线:origin/main @ a1a855a(PR #5065 的 merge 基线)。

事实(逐条 grep 可复核)

packages/runtime/src/http-dispatcher.ts:1610 起:

if (cleanPath === '/openapi.json' && method === 'GET') {
     try {
        const metaSvc = await this.resolveService('metadata', context.environmentId);
        if (metaSvc && typeof (metaSvc as any).generateOpenApi === 'function') {
            const result = await (metaSvc as any).generateOpenApi({});
            return { handled: true, response: this.success(result) };
        }
     } catch (e) { /* ... */ }
}

generateOpenApi 作为方法,全仓 + 两个兄弟仓零实现。 精确名 grep 命中仅 4 处:

位置 性质
http-dispatcher.ts:1613 / :1614 就是上面这个鸭子类型探测本身
packages/spec/src/api/documentation.zod.ts:481 同名但无关——generateOpenApi: z.boolean(),一个配置布尔键,不是方法
packages/spec/src/api/documentation.test.ts:475,544 上述布尔键的测试

MetadataManager / NodeMetadataManager 均无此方法;cloudobjectui 两仓零命中。所以该 if 恒为 false —— 与 #4936matchEndpoint同一类:grep 找得到、运行时永不执行。

但这条路由并非无人服务 —— 这才是重点

packages/rest/src/rest-server.ts:2523真实提供该路由,而且实现是完整的:

  • GET {basePath}/openapi.json → enriched OpenAPI document;
  • base spec 从 @objectstack/spec/openapi.json 惰性加载(rest-server.ts:2683json-schema/openapi.json),该产物由 gen:openapi 生成、并在 spec 的 exports 里作为 ./openapi.json 导出;
  • packages/rest/src/rest-route-ledger.ts:79 有对应台账行(source: 'route-manager')。

于是同一路径有两个属主:rest 侧真能答,dispatcher 侧永远答不了。这正是 ADR-0076「路由与属主」第 1 条点名的形状 —— 「Never add a second implementation of a path that another package already serves… A shadowed duplicate is code that grep finds and the runtime never runs — the exact input that makes an agent (or a human) reason confidently from dead code.」

台账注记本身不准确

packages/runtime/src/route-ledger.ts:252:

{ route: 'GET /openapi.json', domain: '/openapi.json', disposition: 'server-only',
  note: 'docs tooling; falls through when metadata service lacks a generator' },

when metadata service lacks a generator」读起来像是有时有、有时没有。实际是从来没有任何 metadata service 提供过 generateOpenApi,所以是 100% fall through。ADR-0076 第 4 条(machine-readable surfaces must not lie)同样适用于这张台账。

没有验证的一点(不要当成已证事实)

两个属主在真实 composition 下谁先接到请求,我没有实测。这决定了严重度:

  • 若请求先到 rest-server → 用户拿到正确文档,本单纯属死代码 + 台账失准(observation-class);
  • 若在某些 composition 下先到 dispatcher → dispatcher 走完那个恒 false 的 if 后直接落到 this.routeNotFound(cleanPath)(handled: true 的语义 404),用户拿到 404,而 rest 侧那份文档根本没机会出场 —— 这就是用户可见缺陷。

判定这一点需要按 ADR-0076 结尾那句做:「boot the real composition with its real services, or do not claim an answer」—— 起一个真实 showcase boot,curl /api/v1/openapi.json,看返回的是文档还是 404。#4936 正是用这种实测(而非 grep 推断)定的性,建议本单沿用同法再定级。

建议处置(供分诊,未预设)

无论上面哪种结果,dispatcher 那段都应删除 —— 它没有任何情况下是正确属主,/openapi.json 的真实实现在 packages/rest。随之:

  1. route-ledger.ts/openapi.json 行与 LEGACY_CHAIN_PREFIXES 条目一并处理(它们描述的是 dispatcher 侧,而非 rest 侧那张已有台账);
  2. 若实测发现确实存在 404 路径,那就不只是清理,而是修复。

关联


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions