PATCH 返回 200 数据却消失了——内存合并对象不等于数据库真相
用 PATCH 保存一批条目,响应 200、响应体里的数据完全正确,前端状态也没异常。然而用户关了标签页再打开,几条刚保存的数据就蒸发了。反复复现、反复调试,DevTools 里的网络请求白纸黑字写着"成功"——调试方向完全跑偏了。
这个坑真正险恶的地方在于:API 确实成功了,但"成功"指的是请求到达了 handler,不代表数据库里写入了你以为写入的东西。
现象
- 调用
PATCH /api/xxx保存,HTTP 200,响应体包含预期的所有字段和条目数量 - 前端读取响应体、更新本地状态,一切正常
- 刷新页面 / 关标签重开 / 换一台设备访问 → 数据少了几行,或某个字段回滚到旧值
- 再次 PATCH 保存,还是 200 还是看起来正确——但每次都少同样的东西
根因
常见的 handler 写法是这样的:
export async function updateItems(id, patch) {
const current = await getFromDb(id)
const next = {
...current,
...patch,
items: normalizeItems(patch.items), // ← 可能静默过滤
}
await persistToDb(next) // ← 可能静默丢行
return next // ← 返回的是内存对象,不是数据库读回来的
}return next 里的 next 是用请求 payload 在内存里拼出来的,和数据库实际写入了什么完全无关。
如果 normalizeItems 遇到不认识的枚举值返回 null,然后 .filter(Boolean) 把 null 过滤掉,persistToDb 只插入了 13 行——但 next.items 仍然是 14 个,因为它是从原始输入组装的。响应体是谎言,数据库才是真相。
return next 模式之所以流行,是因为省了一次 DB 读回的延迟、写法简洁、单元测试断言 response.body 也能轻松通过。但一旦 persist 路径上有静默过滤(.filter(Boolean)、ENUM 严格模式静默截断、normalizer 返回 null),响应体和 DB 就会永久撒谎,且只在"重新获取"时才暴露。
正解
定位问题:绕开 API,直接查数据库
调试持久化问题时,不要信任 API 响应体,直接查数据库是唯一的权威来源:
# 直接 SQL 查实际行数
SELECT COUNT(*) FROM your_table WHERE entity_id = 'xxx';
# 或用独立 GET 请求绕开前端缓存
curl -H 'Cookie: ...' http://localhost/api/items/xxx | jq '.items | length'如果 DB 行数 ≠ 响应体条目数,说明 persist 路径在静默丢数据。下一步找静默丢弃点:
// 在 normalizer 里加日志
function normalizeItemType(t) {
const result = TYPE_WHITELIST[t]
if (!result) console.warn('[normalizer] unknown type, dropping:', t)
return result ?? null
}修复:handler 写完之后再读一次
新写的 handler 应该在 persist 之后从 DB 重新读取,保证响应体 = 数据库真相:
export async function updateItems(id, patch) {
await persistMerged(id, patch)
return await getFromDb(id) // ← 写完再读,响应 = DB 真相
}多了一次 DB 读,但换来了可调试性和可信的响应体。
如果因性能原因坚持返回内存 next,至少在响应里加 _optimistic: true 标记,并在 API 文档里注明"此响应为乐观值,最终状态以 GET 为准"。UI 消费方在 debug 模式下应在每次保存后强制 refetch 验证。
防御性测试:断言响应体与 DB 一致
const { items: optimistic } = await updateItems(id, patch)
const { items: real } = await getItems(id) // 独立 GET
assert.equal(optimistic.length, real.length, 'response must match DB')这个断言能在 CI 里提前暴露 normalizer 静默丢弃的问题。
一句话外卖
PATCH 响应 200 只证明请求到达了 handler,不证明 DB 写入了你以为写入的东西——调试持久化问题,永远以独立的 DB 查询或 GET 请求为准,响应体只是乐观猜测。