Skip to content

发布出去的 /api/v1/openapi.json 描述的 built-in 路由一条都不存在 —— 10 个 operation 真实 boot 全部 404(证伪 #5456 的「当前未漂移」) #5588

Description

@baozhoutao

发布出去的 GET /api/v1/openapi.json 里,built-in 路由那一段描述的每一条路径都不存在。真实 boot 逐条探测,文档里的路径全部 404;真实路由在另一个前缀上。任何人拿这份文档生成客户端,生成出来的客户端每一个数据调用都会 404。

基线:origin/main @ 5acb93add。在 #5456 的前提复核中量到(#5456 的 body 断言「当前未漂移,7 条与今天的路由面一致」——该断言被本单证伪),按 Prime Directive #10 单独立单。

真实 boot 实测

pnpm dev:crm -- --fresh -p 39177
GET /api/v1/openapi.json  →  200, 151 paths({object} 已按 CRM 对象展开)

逐条探测文档描述的路径 vs 真实路由:

文档里的(展开后)路径                     真实结果
/api/crm_contact                       404   {"error":"Not found"}
/api/meta                              404   {"error":"Not found"}
/api/meta/types                        404   {"error":"Not found"}
/api/.well-known/objectstack           404   {"error":"Not found"}

真实路由                                  结果(401 = 路由在,鉴权挡住)
/api/v1/data/crm_contact               401   {"error":"UNAUTHENTICATED",...}
/api/v1/meta                           401
/api/v1/discovery                      200
/.well-known/objectstack               200   ← dispatcher 在根上服务,不在 /api 下

动词也错。文档写 PUT {object}/{id},真实是 PATCH——服务器对 PUT 明确回 405:

PUT   /api/v1/data/crm_contact/xyz   →  405
PATCH /api/v1/data/crm_contact/xyz   →  401

逐条对照

packages/spec/scripts/build-openapi.ts 的 generateCrudPaths / generateMetadataPaths / generateDiscoveryPaths 用 basePath = '/api' 手写出 7 条 path / 10 个 operation:

base spec operation 真实 rest 路由 判定
GET /api/{object} GET /api/v1/data/:object 路径错(缺 /v1、缺 /data)
POST /api/{object} POST /api/v1/data/:object 路径错
GET /api/{object}/{id} GET /api/v1/data/:object/:id 路径错
PUT /api/{object}/{id} PATCH /api/v1/data/:object/:id 路径错 且动词错(PUT 回 405)
DELETE /api/{object}/{id} DELETE /api/v1/data/:object/:id 路径错
GET /api/meta GET /api/v1/meta 路径错(缺 /v1)
GET /api/meta/types 全仓没有这条路由 描述了一条谁都不服务的路由(最接近的是 GET /api/v1/meta,它返回 types)
GET /api/meta/{type} GET /api/v1/meta/:type 路径错(缺 /v1)
GET /api/meta/{type}/{name} GET /api/v1/meta/:type/:name 路径错(缺 /v1)
GET /api/.well-known/objectstack rest 没有这条路由 dispatcher(packages/runtime/src/dispatcher-plugin.ts:658)在根路径 /.well-known/objectstack 上服务;rest 的 discovery 是 GET /api/v1 + GET /api/v1/discovery

字面比对:0/10 命中。补 /v1 和 /data 之后仍有 3/10 对不上。

反方向同样不等:RestServer.getRoutes() + 两个 direct-mount registrar 枚举出 91 条真实路由;即使只看 base spec 自称覆盖的三个 ledger family(crud 6 + metadata 17 + discovery 2 = 25 条),文档也只描述了 10 个 operation。

为什么没被发现

serve 期的 enrichment(rest-server.ts registerOpenApiEndpoints)只做四件事:覆写 servers[0](只写 origin,不含 basePath)、展开 {object}、合并声明式端点、覆写 info.version。没有任何一步重写 path 前缀。而 check:generated 的收尾台账把 gen:openapi 记为无门禁({ gen: 'gen:openapi', why: 'the OpenAPI document is generated but no check gate compares it to the routes' }),#5168 补的是产物自洽门($ref 能解析、schema 不降级),不看路由。所以两边各自「正确」,合起来全错,全绿。

现有测试还把错误形状钉住了:packages/rest/src/rest-openapi-route.test.ts:125 断言 body.paths['/api/{object}']['x-template'] === true —— 它证明了服务出去的文档字面上就带着 /api/{object}。

处置需要一次契约裁决(本单不预设)

注意 apiPath 是可配置的(api.apiPath ?? api.basePath + '/' + api.version,rest-server.ts:2830),所以 packages/spec 里的静态 JSON 原则上无法对所有部署写对前缀——今天写 /api 只是错得更彻底,写死 /api/v1 也只是对默认部署正确。

关联

Activity

  1. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

    进维护者决策收件箱(cli 车道 PM,session_016FNvXhtSdnEGEfLEsMmvxh,已打 needs-user-decision;domain 标签留分诊席)

    v17 相关性提请注意:这份 /api/v1/openapi.json 是随包发布的对外契约文档,consumer 照它生成客户端会全线 404——若 v17 发布窗口临近,可能需要一个止血决定。

    待拍板三问(dev 的完整三轴论证在 #5456 的 needs_decision 回报,正文有逐条对照表):

    1. built-in 路由段的形状:A 止血改模板(最便宜,一个 PR 可落,但把可配置的 apiPath 焊进静态发布产物——修掉今天这一份,修不掉这一类)/ B 模板相对化 + rest serve 期拼前缀(spec 保持部署无关,仍需 gen:openapi 的 base spec 用 7 条手写 path 描述路由面,与 rest 真实路由无任何对账 —— 漂移了不会红(#5168 的剩余那一半) #5456 的门)/ C built-in 段整体移交 rest 产出(rest 已在产出声明式端点段;ADR-0076 属主 GET /openapi.json 有两个属主:rest-server 真serve,http-dispatcher 的 generateOpenApi 分支全仓无实现(ADR-0076 D1 影子重复) #5078 已确权;结构上消灭第二真相源,gen:openapi 的 base spec 用 7 条手写 path 描述路由面,与 rest 真实路由无任何对账 —— 漂移了不会红(#5168 的剩余那一半) #5456 的门随之不需要存在)。dev 推荐 C 其次 B 反对 A,本席同意——与 fix(rest): 两个端点契约面只宣告匹配器实际会服务的集合 (#5224) #5487 刚立的「faces announce only what the matcher serves」同一原则;若需止血热修可先 A 后 C,但请明示。
    2. 若裁 A/B(仍需对账门):集合定义推荐「逐行处置台账」——复用 rest-route-ledger 现有 disposition 惯例,每条内建路由 documented 或带 review 过的 undocumented 理由,新路由只有一处要填。
    3. 先修再落门 vs 门带背离基线落地:推荐先修——「已知背离」基线会把一份发布出去的错误对外契约登记为有意如此,与 PD chore: version packages #10 冲突;仓内基线 idiom 记录的都是内部债,无一是对外契约。

    拍板后执行路径:裁 C → #5588 按 C 实施、#5456 关闭(门无对象);裁 A/B → #5588 先修、#5456 解锁按台账形状落门。cli 车道随时可派。


    Generated by Claude Code

  2. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    ContributorAuthor

    维护者裁定(2026-08-06,经 cli 车道 PM session_01DWUR56YsttL5sTF72Q75TQ 誊写):C —— built-in 路由段整体移交 rest 产出。spec 只保留它真正拥有的 components.schemas / info / securitySchemes;ADR-0076 属主 rest(#5078 确权),结构上消灭第二真相源。v17 窗口若临近可先 A 止血,但默认直接实施 C。

    执行拆分(两棒,先 rest 后 spec,保证服务出的文档任何时刻不缺 built-in 段):

    1. 第一棒(本单,cli 车道):packages/rest 在 registerOpenApiEndpoints 的 serve 期流水线里丢弃静态产物中过时的 built-in 段、改由自身路由事实(rest-route-ledger / getRoutes())产出正确的 built-in operations(路径含真实前缀、动词 PATCH、不再描述幽灵路由);rest-openapi-route.test.ts:125 等钉住旧形状的 pin 同步翻正为断言新语义(承重翻转,不是删断言)。
    2. 第二棒(另立单转 spec 座位,Blocked-by 本单):packages/spec/scripts/build-openapi.ts 摘除 generateCrudPaths / generateMetadataPaths / generateDiscoveryPaths,发布的静态产物不再携带任何路由描述;发布出去的 OpenAPI 文档 components.schemas 是空的,而 6 个 $ref 全部悬空 —— lazySchema Proxy 撞上 typeof === 'object' 判据 #5168 自洽门随之调整覆盖面。

    连带处置:#5456(对账门)关闭,not planned —— 裁 C 后门无对象(rest 既是路由事实又是文档产出方,无两方可对账);若 C 实施受阻改走 A/B,重开它按台账形状落门。

    标签:摘 needs-user-decision → pm:queue,归 cli 车道排程(v17 相关,rest 包空出后优先)。


    Generated by Claude Code

  3. self-assigned this
    on Aug 6, 2026
  4. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    ContributorAuthor

    认领:PM 循环第 3 轮(cli 车道,v17 优先)
    会话:session_01DWUR56YsttL5sTF72Q75TQ
    分支:claude/issue-5588-openapi-builtin-rest-owned
    Worktree:objectstack-issue-5588
    域:domain:cli(裁决 C 的第一棒,rest 半边)
    文件面:packages/rest(openapi-endpoints.ts / rest-server.ts 的 registerOpenApiEndpoints 流水线 + rest-openapi-route.test.ts 等 pin)+ changeset;⛔ 不触 packages/spec(第二棒 #5744 归 spec 座位,Blocked-by 本单)

    执行注记:①serve 期丢弃静态产物中过时的 built-in 段、由 rest 自身路由事实产出正确段(真实前缀经 getApiBasePath()、PATCH 非 PUT、无幽灵路由);②rest-openapi-route.test.ts:125 等钉旧形状的 pin 承重翻转;③在飞注意:PR #5788(纯 rest 测试替身,已在合并队列)与本单文件面不相交,落地后 merge origin/main 照常;④验收对照 issue 正文的探测表,PR 正文给真实 boot 的前后对比。


    Generated by Claude Code

  5. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    ContributorAuthor

    验收:ACCEPT(第一棒)(cli 车道 PM,session_01DWUR56YsttL5sTF72Q75TQ,2026-08-06 07:0xZ)→ PR #5821

    转 ready + 挂 auto-merge;MERGED 后:①在 #5744 留解锁评论通知 spec 座位(第二棒可动);②rest 线排 #5563(独占窗口,须待 #5673 落地、四包全空)。


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions