Found by the dev on #8441 while filtering the metadata batch verbs' code limb. Unassigned; nobody is on it. Not a defect in #8441's PR — a different producer cluster, deliberately not ridden in.
The measurement
Driven in process against ObjectStackProtocolImplementation with an engine whose delete fails the way a missing table does (Object.assign(new Error('SQLITE_ERROR: no such table: leave_request'), { code: 'SQLITE_ERROR', errno: 1 })), one row through deleteManyData:
{
"success": false, "operation": "delete", "total": 1, "succeeded": 0, "failed": 1,
"results": [{
"id": "r1", "success": false, "index": 0,
"errors": [{ "code": "INTERNAL_ERROR", "message": "SQLITE_ERROR: no such table: leave_request" }]
}]
}
The code half is already correct — toRowApiError gates it on StandardErrorCode ∪ ERROR_CODE_LEDGER, so the driver's SQLITE_ERROR became INTERNAL_ERROR. The message half is ungated:
message: typeof err?.message === 'string' && err.message.length > 0 ? err.message : String(err),
packages/metadata-protocol/src/protocol.ts, in toRowApiError. Three catches feed it — the bulk batchData loop, updateManyData and deleteManyData — each pushing errors: [toRowApiError(err)] onto a per-row result.
Why this is a producer neither prior card covered
Same class as #8136 (the uninstall/overlay cluster) and #8333 (the metadata batch verbs P6–P13): the text rides response DATA, not a thrown message, so no HTTP boundary's 5xx withhold reaches it. But the sink is different — BatchOperationResult.errors[] on a data batch write, not failed[] on a metadata verb — and it is in neither card's enumeration. #8333's table stops at P13 and every one of its entries is a metadata verb.
The producer-side rule those cards installed applies unchanged: clientFacingFailureText(err, fallback) already exists in this file and asks the bounded question — did this error declare itself a client-facing refusal (a 4xx status)?
What a taker must check first, because it is the whole risk
⚠️ This catch carries a large authored population, larger than #8333's sites did, and blanking it would be a real usability regression. Measured in passing: updateManyData on a missing row never reaches the catch at all — a pre-check answers RECORD_NOT_FOUND / 404 directly — but the catch also receives rowRequiredIdError (VALIDATION_FAILED / 400), recordNotFoundError, and every validation refusal a hook or the schema raises. Whether each of those declares a 4xx status is the measurement that decides whether the rule can be applied here or whether some producer needs declaring first, exactly as #8333 had to do for the seed request. Do not sweep it blind.
Note also that httpStatus is absent from the measured envelope above — toRowApiError only sets it when the caught error carried a status, which a bare driver throw does not. That is the same signal the rule keys on, so the two facts are consistent.
Scope note
code is already correct here and needs nothing. This is the message half only.
Related
#8136 (option C, the measured cluster) · #8333 (the metadata batch verbs, PR #8436) · #8441 (the code limb, where this was found) · #8442 (seed-loader's own errors[].message, a different file) · #8086 (the ruling recording option C)
Generated by Claude Code
Found by the dev on #8441 while filtering the metadata batch verbs'
codelimb. Unassigned; nobody is on it. Not a defect in #8441's PR — a different producer cluster, deliberately not ridden in.The measurement
Driven in process against
ObjectStackProtocolImplementationwith an engine whosedeletefails the way a missing table does (Object.assign(new Error('SQLITE_ERROR: no such table: leave_request'), { code: 'SQLITE_ERROR', errno: 1 })), one row throughdeleteManyData:{ "success": false, "operation": "delete", "total": 1, "succeeded": 0, "failed": 1, "results": [{ "id": "r1", "success": false, "index": 0, "errors": [{ "code": "INTERNAL_ERROR", "message": "SQLITE_ERROR: no such table: leave_request" }] }] }The
codehalf is already correct —toRowApiErrorgates it onStandardErrorCode ∪ ERROR_CODE_LEDGER, so the driver'sSQLITE_ERRORbecameINTERNAL_ERROR. The message half is ungated:packages/metadata-protocol/src/protocol.ts, intoRowApiError. Three catches feed it — the bulkbatchDataloop,updateManyDataanddeleteManyData— each pushingerrors: [toRowApiError(err)]onto a per-row result.Why this is a producer neither prior card covered
Same class as #8136 (the uninstall/overlay cluster) and #8333 (the metadata batch verbs P6–P13): the text rides response DATA, not a thrown message, so no HTTP boundary's 5xx withhold reaches it. But the sink is different —
BatchOperationResult.errors[]on a data batch write, notfailed[]on a metadata verb — and it is in neither card's enumeration. #8333's table stops at P13 and every one of its entries is a metadata verb.The producer-side rule those cards installed applies unchanged:
clientFacingFailureText(err, fallback)already exists in this file and asks the bounded question — did this error declare itself a client-facing refusal (a 4xxstatus)?What a taker must check first, because it is the whole risk
updateManyDataon a missing row never reaches the catch at all — a pre-check answersRECORD_NOT_FOUND/ 404 directly — but the catch also receivesrowRequiredIdError(VALIDATION_FAILED/ 400),recordNotFoundError, and every validation refusal a hook or the schema raises. Whether each of those declares a 4xx status is the measurement that decides whether the rule can be applied here or whether some producer needs declaring first, exactly as #8333 had to do for the seed request. Do not sweep it blind.Note also that
httpStatusis absent from the measured envelope above —toRowApiErroronly sets it when the caught error carried astatus, which a bare driver throw does not. That is the same signal the rule keys on, so the two facts are consistent.Scope note
codeis already correct here and needs nothing. This is the message half only.Related
#8136 (option C, the measured cluster) · #8333 (the metadata batch verbs, PR #8436) · #8441 (the
codelimb, where this was found) · #8442 (seed-loader's ownerrors[].message, a different file) · #8086 (the ruling recording option C)Generated by Claude Code