Skip to content

REST PATCH /data/:object/:id:请求体里的标量 id 压过路径 :id,存在性探测/OCC 判在一行、写落在另一行、响应报第三个说法 #6479

Description

@baozhoutao

范围外发现,来自 #6435 / PR #6475 的实施过程(PD #10)。#6435 的范围钉在 by-id 臂里非标量载荷 id 的剥离(路线 A 明确写着"标量 data.id 现状不动"),所以本条未在该 PR 内修改,只记录。

证据来源:静态读源码(file:line),未跑端到端 HTTP 复现。 两个组成事实各自都被现有测试钉着,组合未实测。

事实

PATCH /data/task/rec_1,请求体 {"id": "rec_2", "title": "x"},今天会:

  1. 存在性探测判 rec_1packages/metadata-protocol/src/protocol.ts:5636updateData:const current = await this.probeRecord(request.object, request.id),request.id 来自路径参数;if (!current) throw recordNotFoundError(...)
  2. OCC 判 rec_1。紧接着 this.assertVersionOf(request.object, request.id, current, request.expectedVersion)——同样是路径 id。
  3. 写落到 rec_2。同一函数下面:const opts = { where: { id: request.id } }await this.engine.update(request.object, request.data, opts)。而 resolveEngineUpdateDispatch 的规则是载荷优先:真值标量 data.id 压过 where.idENGINE_UPDATE_DISPATCH_CASES 里就有这一行:
{ what: 'a SCALAR data.id still wins over a scalar where.id',
  data: { id: 'rec_1', title: 'x' }, options: { where: { id: 'rec_2' } },
  expect: 'by-id', expectId: 'rec_1' }

即引擎绑定的是请求体里的 rec_2,driver.update(task, 'rec_2', …)
4. 响应说 rec_1return { object, id: request.id, record: result }——id 是路径 id,record 却是 rec_2 写后的回读。

于是 URL 说一行、写落在另一行、响应里 idrecord 互相矛盾;并且 rec_2 从未被存在性探测,也从未被 OCC 校验(客户端送的 If-Match 是 rec_1 的版本,却放行了对 rec_2 的写)。

为什么不是理论输入

请求体原样从线上传到引擎,中间没有任何一层剥 id:

  • packages/rest/src/rest-server.tsPATCH /data/:object/:id(约 :5268)只从 body 里剥 expectedVersion,id 不动;
  • packages/spec/src/api/protocol.zod.ts:519UpdateDataRequestSchemadata 声明为 z.record(z.string(), z.unknown()),任何 id 都通过。

而客户端 GET 一条记录、改字段、整份 PUT 回来是最常见的写法之一——只要它错拿了另一条记录的 id(列表里点错一行、并发刷新后 id 串了、AI 生成的客户端拼错),就变成一次静默的跨行写。

同仓库里另一条 ingress 已经把这件事做对了,可作对照:批量路径 packages/rest/src/rest-server.ts:8523 写的是

ql.update(op.object, { ...data, id }, { context: trxCtx, onFieldsDropped })

id(路径/操作里的那个)排在 spread 之后,所以它赢。两条 ingress 对同一个问题给了两个答案,正是 #4550 / #4434 那一族。

与既有单的关系

方向(不预设结论)

  • A:ingress 侧以路径 :id 为准——updateData{ ...request.data, id: request.id }(与批量路径 :8523 同形),或先把 body 的 id 剥掉。最小、与仓库内既有对照一致。
  • B:ingress 侧响亮拒绝——body 带了 id 且与路径 :id 不等时返回 400,点名两者冲突。诊断最好,但对"整份 PUT 回来"的常见写法(body id 与路径 id 相同)必须放行,所以是"不等才拒"。
  • C:在 UpdateDataRequestSchema 层面禁止 data 携带 id。契约最干净,但会打断上面那种合法回写,且是 packages/spec 的改动。

严重度请分诊裁:这是一次静默的跨行写,且绕过了 OCC,但触发要求调用方在 body 里放一个与路径不同的真值标量 id

关联:#6435 / PR #6475(by-id 载荷剥离,本条的发现来源)、#5748 / PR #5919(载荷优先的派发规则)、#4435(updateData 的存在性探测与 OCC 共用一次读)、#5922(载荷值校验,另一条轴)、#4550 / #4434(为什么共享谓词而不是第二个答案)。

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