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(为什么共享谓词而不是第二个答案)。

Activity

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