Skip to content

fix(metadata-protocol): a batch row's httpStatus reads the declared status, not one spelling of it (#8570) - #8633

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-8570-row-http-status-declarations
Aug 14, 2026
Merged

fix(metadata-protocol): a batch row's httpStatus reads the declared status, not one spelling of it (#8570)#8633
os-zhuang merged 4 commits into
mainfrom
claude/issue-8570-row-http-status-declarations

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#8570

What was wrong

toRowApiError (packages/metadata-protocol/src/protocol.ts) set a batch failure row's httpStatus from err.status alone. Two producers that reach those catches declare a genuine client refusal without that spelling, so their rows shipped with no status at all — while sibling rows of the same response carried one:

producercode.status.statusCodevalidation shapebeforeafter
rowRequiredIdErrorVALIDATION_FAILED400no400400
recordNotFoundErrorRECORD_NOT_FOUND404no404404
objectql ValidationErrorVALIDATION_FAILEDyes400
plugin-approvals record lockRECORD_LOCKED409no409
app hook throwing a bare Errorno
driver fault (SqliteError, …)SQLITE_*no

A caller branching on httpStatus to tell "fix your input" from "the server broke" got an answer for some failure rows and silence for others, with nothing saying which. It is the same single-spelling defect that #7525 repaired at the HTTP door, one layer down.

The change

httpStatus now comes from resolveThrownHttpError (@objectstack/types) — imported, the resolver the HTTP doors answer with and the one the row's message limb already delegates to (#8502). No second local chain: this function had three fields and would otherwise have carried three derivations of "what status is this throw".

declaredStatus, not status. That resolver answers for every throw, and for an undeclared one its status is the caller's fallback, 500. Stamping it would put httpStatus: 500 on the last two rows of the table above — rows that never carried one — which is an addition to the wire for those populations, not the restoration of a declared value. So ThrownHttpError gains declaredStatus: the same resolution minus the fallback, absent when the throw declared nothing. status is untouched and every boundary that answers with the status itself keeps reading it. The field also replaces a magic-number workaround already in the tree — resolveThrownHttpError(e, 0).status !== 0 in packages/rest's publish-classification suite asks exactly this question with a sentinel.

Two deliberate boundaries, both pinned:

  • The gate is declared-ness, not the 4xx band the message limb uses. That limb decides disclosure of free text, where a 5xx must be withheld; this one reports a number the producer authored, and a row already ships httpStatus: 503 today when the same refusal spells .status. Narrowing to 4xx would withdraw a status the wire carries — a different decision from this one.
  • code reads the same resolution, so a row cannot contradict itself. Deriving code from err.status while httpStatus came from the resolver would answer { code: 'INTERNAL_ERROR', httpStatus: 409 } for a statusCode-spelled refusal whose own code the ledger does not know.

Docs rider (triage, in scope)

ApiErrorSchema.httpStatus now documents what absence means — "no claim made", never a status of its own and never 200 — and what it means where the envelope rides response DATA rather than the response line: on a batch row, present = the throw declared a status, absent = an undeclared server-side fault the caller should treat as a 500.

Verification

Both card rows reproduced on the real stack, then ablated in both directions.

  • Real stack, row 1packages/runtime/src/batch-row-http-status-real-driver.integration.test.ts: a real ObjectQL over a real SqlDriver (better-sqlite3), the engine's own validator rejecting a 15-character value against maxLength: 4, giving the card's row verbatim (name must be ≤ 4 characters (got 15)) — now with httpStatus: 400. The thrown error is asserted to spell no status in either channel, so the row gains one only through the shape it declares.
  • Real stack, row 2packages/plugins/plugin-approvals/src/record-lock-batch-row-status.integration.test.ts: the REAL bindApprovalLockHook hook, a real pending sys_approval_request row, a real sqlite store, through the real updateManyData and batchData loops → { code: RECORD_LOCKED, message: …, httpStatus: 409 }. The hook's throw is measured in place (own properties [stack, message, code, statusCode], status undefined).
  • Ablation (a), fix reverted: 8 pins red across the two metadata-protocol files, each losing exactly the newly-populated status (expected undefined to be 400 / 409 / 503); the undeclared-population pins stayed green, as they must.
  • Ablation (b), over-broad (status stamped unconditionally): the undeclared populations gain httpStatus: 500 and are caught — 2 pins in protocol.batch-row-http-status.test.ts §3 and the real-driver SqliteError pin in runtime. This is the direction that separates "populates the declared refusals" from "populates everything", and it is why §3 exists.

Suites: metadata-protocol 1286, spec 10527, objectql 3579, rest 1895, runtime 2373, types 280, plugin-approvals 462 — all green. Typecheck green for types / spec / runtime / plugin-approvals.

packages/plugins/plugin-approvals gains a vitest.config.ts aliasing @objectstack/metadata-protocol to source: the new integration test imports it as a value, and check:test-source-alias's prescribed fix for that is the alias, never a new entry in its shrink-only registry (check:test-source-alias verified green).


Generated by Claude Code

os-zhuangand others added 2 commits August 14, 2026 03:16
…tatus, not one spelling of it (#8570)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
@vercel

vercelBot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 14, 2026 5:09am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-protocol, @objectstack/plugin-approvals, @objectstack/spec, @objectstack/types.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/plugin-approvals, @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-protocol, packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/metadata-protocol, @objectstack/plugin-approvals, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/plugin-approvals, @objectstack/spec, @objectstack/types)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/plugin-approvals, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/metadata-protocol, @objectstack/plugin-approvals, @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claudeand others added 2 commits August 14, 2026 04:43
…ow pin
`registry.registerObject` takes (schema, packageId, …). The new integration
test omitted the second argument, which the package's own `typecheck` script
cannot see — its tsconfig excludes `**/*.test.ts` — while the TEST_DEBT ratchet
measures tsc WITH the test layer in the program and counted the +1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
@os-zhuang
os-zhuang marked this pull request as ready for review August 14, 2026 05:30
@os-zhuang
os-zhuang added this pull request to the merge queueAug 14, 2026
Merged via the queue into main with commit 9f5cc79Aug 14, 2026
29 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8570-row-http-status-declarations branch August 14, 2026 05:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants

@os-zhuang@claude