Skip to content

A batch row's httpStatus reads only .status, so two genuine 4xx populations ship a row with no status at all #8570

Description

@os-zhuang

Found while measuring #8502's population on the real stack. Unassigned; nobody is on it. Not a defect in #8502's PR — that card is the message limb only, and this is the httpStatus limb beside it, deliberately not ridden in.

The measurement

toRowApiError (packages/metadata-protocol/src/protocol.ts) sets httpStatus from one field:

conststatus=typeoferr?.status==='number' ? err.status : undefined;
...
...(status!==undefined ? {httpStatus: status} : {}),

Driven on the real stack — a real ObjectQL over a real SqlDriver, through all three bulk-write loops — two producers that reach these catches declare a client refusal without.status:

producercode.status.statusCodevalidation shaperow's httpStatus
objectql ValidationErrorVALIDATION_FAILEDyesabsent
plugin-approvals record lockRECORD_LOCKED409noabsent

Measured rows, verbatim:

{ "code": "VALIDATION_FAILED", "message": "name must be ≤ 4 characters (got 15)" }
{ "code": "RECORD_LOCKED", "message": "RECORD_LOCKED: record 'ok1' of 'm8502_task' is locked while an approval is in progress" }

Both are well-defined client refusals — 400 and 409 respectively — and a caller branching on httpStatus to tell "fix your input" from "the server broke" gets nothing for either. The sibling rows in the same response do carry it (rowRequiredIdError → 400, recordNotFoundError → 404), so within one batch the field is present for some failure rows and absent for others with no signal saying which.

resolveThrownHttpError (@objectstack/types) already answers this exact question and reads all three declarations — it is what the HTTP doors use, and what #8502 routed the message limb through. The httpStatus limb is the only one of the row's three fields still reading a single spelling:

Why it was left out of #8502

Adding httpStatus where the wire did not previously carry one is an addition to the response, not a withhold — the same reasoning #8441 recorded for declining to mint a code where none existed. It wants its own decision about whether BatchOperationResult consumers can absorb a newly-present field, and possibly a note in the ApiError contract about what absence means. So it is filed rather than ridden in.

Not a disclosure issue, unlike its two siblings — this is a usability/contract gap.

Related

#8502 (the message limb, PR #8569) · #8441 (the code limb) · #7525 (the same single-spelling defect at the HTTP door, fixed there) · ADR-0112


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions