Skip to content

GET /openapi.json 有两个属主:rest-server 真serve,http-dispatcher 的 generateOpenApi 分支全仓无实现(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 均无此方法;cloud、objectui 两仓零命中。所以该 if 恒为 false —— 与 #4936 的 matchEndpoint 是同一类:grep 找得到、运行时永不执行。

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

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

  • GET {basePath}/openapi.json → enriched OpenAPI document;
  • base spec 从 @objectstack/spec/openapi.json 惰性加载(rest-server.ts:2683 拼 json-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

Activity

  1. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    分诊定级:观察类(死代码 + 台账注记失准),无用户可见缺陷 —— 实测已按本单要求完成(维护者批准,2026-08-04)

    真实 boot 定级(worktree @ origin/main = 29c6c9d,showcase 47 plugins,RestAPI 为 #22、Dispatcher 为 #23 同组合共存):

    • GET /api/v1/openapi.json(匿名与带 cookie 一致)→ 200,355,900 字节的真 OpenAPI 3.1.0 文档。三个指纹坐实应答者是 rest-server:① servers[0] 是按 Host 头注入的 http://localhost:39217(磁盘 base spec 只有 localhost:3000);② paths 7 → 199,含 46 条 /api/showcase_* 展开({object} 占位符按 getMetaItems 展开是 rest-server 的行为);③ 恰好 2 条 "x-template": true(rest-server 独有写入)。
    • dispatcher 分支是「双重死」:除本单已证的 generateOpenApi 零实现外,dispatcher-plugin.ts 与 domain registry 均无任何路由把 /openapi.json 送进 dispatch() —— 该分支在 HTTP 上不可达,不存在属主竞争,rest-server 独占应答。
    • 形态对照完备:未路由路径均为 Hono 裸 {"error":"Not found"};dispatcher 语义包络在活路由上可复现(/automation/nope 的 RESOURCE_NOT_FOUND 等),证明判别方法有效。

    处置:不入 pm:queue,并入 #5040 E6 执行(该单已重定义:endpoint summary/description 走 rest-server 既有 enrichment 管线;dispatcher 死分支 + route-ledger.ts:252 失准注记随 E6 一并摘除/修正 —— 见 #5040 设计修正评论)。

    boot 中顺带记录的次要观察(除第一条已立单外,余者未立单及理由):

    1. POST /api/v1/auth/login(不存在的 auth 子路径)→ 500 INTERNAL_ERROR: "request.headers.get is not a function" 而非干净 404/405,且内部 TypeError 文案外漏 —— 已立单(见关联);
    2. GET /api/v1/(尾斜杠)404 而 /api/v1 200 —— 装饰性,未立单;
    3. 全部探测路径均未能引出 ROUTE_NOT_FOUND 语义包络(12 条样本,非穷举)—— 与 17.x 立项:建设声明式 ApiEndpoint 执行器(挂载 + matchEndpoint + authRequired/cacheTtl/inputMapping/outputMapping 逐键接线) #5040 设计 §1-C 的「裸/语义 404 分裂收口」互为佐证,记录于该卡;
    4. GET /api/v1/meta/bogus-nope → 200 空集 —— 动态可注册类型系统下「未知类型 = 空集」可能即为设计语义,不作缺陷预判;如需裁定另立。

    Generated by Claude Code

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

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