Skip to content

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

Closed

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. added
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 4, 2026
  2. 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

    Labels

    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions