Skip to content

deleteManyData's row errors carry raw driver text in errors[].message — the data-batch producer #8136/#8333 never reached #8502

Description

@os-zhuang

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 correcttoRowApiError 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

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions