Repository navigation
设计:批量写路由的 405 兜底与导入不对称 —— data-objectstack 直接 rethrow,用户看到硬报错 #3757
Description
Activity
更正:本 issue 的前提是错的,现在拿到 objectui 源码核实了
开这个 issue 时我没有 objectui 源码,依据的是 #3391 P1 方案评论里的一句勘查结论。现在源码在手,逐条走完调用链后确认:没有任何 UI 路径会把批量写的 405 抛给用户。原正文说的「适配器直接 rethrow → Setup 网格多选删除硬报错」不成立。
实际的调用链
层 行为 data-objectstackbulkUpdate/bulkDelete(packages/data-objectstack/src/index.ts:1100-1102、:1150-1152)确实 throw normaliseClientError(err)—— 原正文这一句是对的唯一调用方 useBulkExecutor(packages/plugin-grid/src/hooks/useBulkExecutor.ts:191-195)→executeBulkBatch(packages/core/src/actions/bulkFastPath.ts:93-110)try { bulkCall } catch { /* Fall through to per-row */ }—— 对任何异常都回退逐行,405 也不例外console 对象网格的多选删除( packages/app-shell/src/hooks/useObjectActions.ts:96-100)Promise.allSettled(records.map(r => dataSource.delete(...)))—— 根本不调bulkDelete,一开始就是逐行单删bulkOperation()(create分支无回退)全仓 packages/apps里 零调用方所以适配器那个 rethrow 被上游两层各自兜住了:一层显式 catch 回退,另一层压根不走批量路由。
这也意味着 #3745 的用户影响被我说大了
#3745(补
bulk原语)修的 API 层不对称是真的,我用真实服务打过 14/14 的矩阵:deleteMany在那 8 个对象上确实 405,而同样的单条写完全开放。但那个 PR 说明里「Setup 网格多选删除会直接报错」是我沿用了同一条错误前提 —— 实际表现是静默降级为逐行删除,用户看不出异常。修复本身仍然正确(API 契约自洽、/batch恢复可用),只是没有当时描述的那种用户可见故障。剩下的真问题(比原正文小得多,但仍值得定)
- 回退是完全静默的。
executeBulkBatch的catch {}不区分原因:bulk未授予、网络错、鉴权失败,一律变成 N 次逐行写。代价是丢原子性(一次deleteMany拆成 N 个独立事务)且无任何信号。至少该区分「405 = 该对象没开批量,回退是预期的」与「其它错 = 真故障」。 - 按钮层其实已经可以压制了。
/me/permissions已下发apiOperations,resolveCrudAffordances的第二参 AND 已经在跑 —— 我在浏览器里实测过:两个同为 platform 桶、UI 轴完全相同的对象,仅因服务端 effective 集不同,New/Import按钮就一个显示一个隐藏(截图见 refactor(metadata-core): 回收 sys_view_definition 的等价全开白名单 + 派生契约端到端实测 (#3026) #3772)。同样的机制可以用来在bulk不在 effective 集时直接不渲染批量操作入口,而不是让它先发请求再静默降级。 createMany那条无回退分支目前没有调用方,属于潜在坑而非现存问题 —— 要么接上回退,要么删掉。
建议
原正文的 4 个决策点里,第 1 条(回退还是终止)现在有了确定答案:已经在回退了,要定的是「回退该不该有声音、以及只对 405 回退还是对所有错回退」。第 3 条(按钮层压制)机制已就绪,是最小改动的正解。第 2 条(文案落点)只在决定要出声时才需要。第 4 条已查明:
createMany无调用方。正文我不改,保留原始记录;以本条评论为准。
Generated by Claude Code
- 回退是完全静默的。
二次更正:准备动手实现时发现两半都不成立,建议关掉
拿到 objectui push 权限、准备实现上一条评论列的两项时,又核实了一遍源码。两项都站不住。
❌ 「按钮层按
bulk压制」—— 已经做了,而且按bulk压是错的已经做了:
packages/plugin-grid/src/ObjectGrid.tsx:1591-1602(#3720)已经把内建批量删除入口挂在objectCanDelete上,而objectCanDelete来自resolveRowCrudAffordances({ …, effectiveApiOperations })(rowCrudAffordances.ts:75-80)—— 正是/me/permissions下发的 effective 集。注释写得很明确:author 声明的bulkActions: ['delete']是接线声明,不是权限授予,verdict 关掉时会被过滤掉。而且按
bulk压会是回归:门控挂的是子动词delete,不是bulk。这是对的 ——bulk未授予但delete已授予时,逐行回退让这个操作照常可用;按bulk隐藏入口等于把一个能用的功能藏起来。我上一条建议这么做,是错的。❌ 「回退是静默的,该区分 405 与真故障」—— 前提也不准
executeBulkBatch的catch {}确实吞掉异常对象,但错误并没有消失:回退后的Promise.allSettled会把每一行的失败连同res.reason.message收进outcome.errors(bulkFastPath.ts:112-123),经BulkResult.errors报给用户。所以 403/400/500 的场景不是"静默降级",而是"从 1 条批量错误变成 N 条逐行错误" —— 而那句注释// Fall through to per-row — gives the user actionable detail说的正是这个有意的取舍:逐行能告诉用户哪几行、为什么失败,比一条不透明的批量错误信息量更大。原子性那条也比我想的弱:回退只在批量已经失败之后触发,此时 all-or-nothing 已经落到 "nothing",逐行把它变成 "some",且每行结果如实上报。走 bulkCall 的只有 update/delete(
useBulkExecutor.ts:191-195),没有 create,所以不存在重复创建的风险。剩下的真实代价只有:403 这类"逐行必然也失败"的情况会白烧 N 个请求。这不值得为它改动一条有明确设计理由的容错路径。
结论
本 issue 从开出来就建立在一条我未经核实就沿用的勘查结论上(#3391 P1 方案评论),两轮核实后:用户可见故障不存在、按钮门控已就位且形态正确、静默回退的指控不成立。建议以
not planned关闭。唯一残留的小事,不足以撑起一个 issue,记在这里备查:
data-objectstack的bulkOperation()里create分支没有逐行回退,而该函数在packages/apps全仓零调用方 —— 是个没被触发的坑,将来若接线需注意,或直接清理掉。
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
问题
同样是「对象未开放某操作」的 405(
OBJECT_API_METHOD_NOT_ALLOWED),前端两条路径的处理方向相反:isImportNotAllowed,四个 catch 点先于 unsupported 判定,命中即终止并出grid.import.notAllowed专用文案(objectui#2823 / #3391 P0)updateMany/deleteMany)data-objectstack适配器直接 rethrow(packages/data-objectstack/src/index.ts约:1100-1102/:1150-1152);逐行回退只在客户端方法不存在时触发,405 不触发这个不对称正是 #3745 那个 bug表现成"硬报错"而不是"降级慢一点"的原因:8 个对象缺
bulk原语 → 多选删除 → 405 → 适配器 rethrow → Setup 网格直接报错。#3745 修了那 8 个对象,但适配器这一侧的行为没动 —— 下一个缺bulk的对象(或第三方应用里刻意只开单条写的对象)还会撞上同样的体验。需要先定的设计问题
不是「照抄一行 405 判定」那么直接,建议先定调:
deleteMany是一个批,拆成 N 次单删就是 N 个独立事务,中途失败会留下部分完成的状态。批量删除尤其危险。倾向:不回退,给独立文案(与导入侧的 405 处理同构),但需要明确写下来。grid.import.notAllowed+ 10 locale)。批量写的调用点分散(网格多选工具栏、列表页、可能还有详情页),文案是放适配器(统一但离 UI 远)还是放各调用点(贴近场景但要维护 N 处)?/me/permissions已下发 per-objectapiOperations(feat: single-source API-method derivation contract (#3391 P1) #3498),前端已经能知道某对象不支持bulk—— 那么"多选删除"按钮本可以像 Import/Export 一样按 effective 集隐藏/禁用,根本不让用户点到 405。这才是与 设计:UI 操作按钮与 enable.apiMethods 白名单的前后端一致性契约 #3026 契约一致的做法(按钮谓词 =effective.api(op) ∧ affordance(op) ∧ can(user, 基础权限)),405 文案只作为兜底。倾向:两者都做 —— 按钮层压制是主路径,405 文案是兜底(权限可能在页面打开后才收紧)。createMany呢? 上表只列了 update/delete,createMany的路径要一并确认是否同样 rethrow。可复用的东西
isImportNotAllowed谓词(objectui,ImportWizard)—— 405 判定逻辑可直接复用,注意不要并进isUnsupportedImportJob:404(路由不存在→回退)与 405(未开放→终止)语义相反,这个坑 跟踪:UI 操作按钮与 apiMethods 白名单一致性契约落地(#3026 设计定稿) #3391 P0 已经踩过一次。resolveCrudAffordances(obj, effectiveApiOperations?)第二参(objectui#2823 已铺路)—— 问题 3 的按钮层压制接这里,和 Import/Export 同一条路。getObjectApiOperations+check()映射(objectui permissions)。关联
bulk原语 (#3026) #3745(8 个对象缺bulk,本 issue 的触发案例)、设计:UI 操作按钮与 enable.apiMethods 白名单的前后端一致性契约 #3026(UI 按钮 ↔ 白名单一致性契约)、跟踪:UI 操作按钮与 apiMethods 白名单一致性契约落地(#3026 设计定稿) #3391 P1(bulk 门禁bulk ∧ child;P0 的导入侧 405 文案)