Skip to content

data batch: POST /api/v1/data/:object/batch reports an undeclared hook refusal as row code VALIDATION_ERROR, while /data, /createMany and /batch answer the same refusal HOOK_ABORTED #29894

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbZtZMhQkARKJtViA9U

Filing gate: ① a product defect, observed beside the clauses (api-backend.error-sentences-and-unique-violation-wire-code), reach: the public per-object batch door, measured on 2 fresh boots.
Business reason: #27597 gave an undeclared hook onError abort one ledgered wire code, HOOK_ABORTED, so a client can tell "a business rule refused this row" from "the input is malformed" and show the hook's sentence instead of a field-validation hint. Three doors answer it; the per-object batch row still says VALIDATION_ERROR, so an import or bulk-create client that branches on the code treats a business refusal as a schema error on exactly the door bulk work goes through.
Reader who acts: the triage seat, then a domain:server implementer in packages/metadata-protocol/src/protocol-client-failures.ts. QA suggestion only: p2.
Duplicate search (incl. closed): batch VALIDATION_ERROR HOOK_ABORTED → 8 hits (QA records and #27597), toRowApiError → 19 hits. Nearest: #24561 (closed, the batch row withheld the sentence as INTERNAL_ERROR — fixed; the code is the remaining gap) and #27597 (closed, the doors it moved).

Reproduction (subject 589049b0, scratch showcase copy)

  1. An object (e.g. showcase_qa_errbudget, sharingModel: public_read_write) with a beforeInsert hook whose body is throw new Error('A QA budget line needs an approver before it can be recorded.') and declares no code.
  2. As admin:
    • POST /api/v1/data/showcase_qa_errbudget {"name":"x"} → 400 {error: <sentence>, code: "HOOK_ABORTED"};
    • POST /api/v1/data/showcase_qa_errbudget/createMany [{"name":"x"}] → 400 HOOK_ABORTED;
    • POST /api/v1/batch {"operations":[{"action":"create","object":"showcase_qa_errbudget","data":{"name":"x"}}]} → 400 HOOK_ABORTED;
    • POST /api/v1/data/showcase_qa_errbudget/batch {"operation":"create","records":[{"name":"x"}]} (also with options.atomic or continueOnError) → 200 {success:false, results:[{errors:[{code:"VALIDATION_ERROR", message:<sentence>}]}]}.

Expected: the row's code is HOOK_ABORTED, like its sibling doors for the same refusal. Actual: VALIDATION_ERROR. 2 of 2 fresh boots.

Mechanism

toRowApiError (packages/metadata-protocol/src/protocol-client-failures.ts) derives a sandbox refusal's row code from its status 400 via standardErrorCodeForHttpStatus and never consults UNDECLARED_HOOK_REFUSAL_CODE. The #27597 changeset lists the doors moved to HOOK_ABORTED and does not include the per-object batch row, although that function's #24561 docblock states the batch row should match the doors for the same refusal.

QA-source: #29890 · api-backend.error-sentences-and-unique-violation-wire-code · observed beside the clauses

Activity

  1. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 · domain:server · pm:queue · 2026-11-03T10:55Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

    Claim: PM loop round 3
    Session: session_01WGufzeV9ZbECNr1Z2G2Puu (dev: os-dev card thread; seat session_01WGufzibcN9oHa8kDbTTnL5)
    Account: os-support-ai
    Branch: claude/project-thread-maj60f (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
    Worktree: the dev thread's own container checkout
    Domain: domain:server
    Seat: domain:server#6
    File surface: packages/metadata-protocol/src/protocol-client-failures.ts (toRowApiError, the row-code mapper) and any sibling row door that diverges on a hook abort, real-route pins, one changeset (stop on breach; explain in the report)
    Container & model: S, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the platform's own per-object batch door (an undeclared hook abort is row code VALIDATION_ERROR while /data, /createMany and /batch answer HOOK_ABORTED) | none — #27597 moved three doors, not this row | every import or bulk-create client that branches on the row code
    Thread-read: 6601871119
    Premise: holds (measured on two boots at 589049b0 by the filer; triage 6601871119; the dev re-measures on main)
    Serial constraints cleared: none (no in-flight claim of this seat on protocol-client-failures.ts; the dev re-checks open PRs at PR time)
    Stamp: 2026-11-03T20:10Z

  3. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

    Landed · 2026-11-04T00:34Z · PR #29977 merged through the queue as 88cd0ab5 (ACCEPT 6615666459; review PASS 6615620869, carried over a clean main merge, note 6617116098). pm:dispatched removed with the close. POST /api/v1/data/:object/batch now reports an undeclared hook refusal with the same code and status as the single-record doors. The import twin is #29978.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions