Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): a batch row's httpStatus reads the declared status, not one spelling of it (#8570) - #8633
Conversation
…tatus, not one spelling of it (#8570) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 4 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…-http-status-declarations
…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
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8570
What was wrong
toRowApiError(packages/metadata-protocol/src/protocol.ts) set a batch failure row'shttpStatusfromerr.statusalone. 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:.status.statusCoderowRequiredIdErrorrecordNotFoundErrorValidationErrorErrorSqliteError, …)A caller branching on
httpStatusto 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
httpStatusnow comes fromresolveThrownHttpError(@objectstack/types) — imported, the resolver the HTTP doors answer with and the one the row'smessagelimb 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, notstatus. That resolver answers for every throw, and for an undeclared one itsstatusis the caller's fallback, 500. Stamping it would puthttpStatus: 500on 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. SoThrownHttpErrorgainsdeclaredStatus: the same resolution minus the fallback, absent when the throw declared nothing.statusis 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 !== 0inpackages/rest's publish-classification suite asks exactly this question with a sentinel.Two deliberate boundaries, both pinned:
messagelimb 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 shipshttpStatus: 503today when the same refusal spells.status. Narrowing to 4xx would withdraw a status the wire carries — a different decision from this one.codereads the same resolution, so a row cannot contradict itself. Derivingcodefromerr.statuswhilehttpStatuscame from the resolver would answer{ code: 'INTERNAL_ERROR', httpStatus: 409 }for astatusCode-spelled refusal whose own code the ledger does not know.Docs rider (triage, in scope)
ApiErrorSchema.httpStatusnow 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.
packages/runtime/src/batch-row-http-status-real-driver.integration.test.ts: a realObjectQLover a realSqlDriver(better-sqlite3), the engine's own validator rejecting a 15-character value againstmaxLength: 4, giving the card's row verbatim (name must be ≤ 4 characters (got 15)) — now withhttpStatus: 400. The thrown error is asserted to spell no status in either channel, so the row gains one only through the shape it declares.packages/plugins/plugin-approvals/src/record-lock-batch-row-status.integration.test.ts: the REALbindApprovalLockHookhook, a real pendingsys_approval_requestrow, a real sqlite store, through the realupdateManyDataandbatchDataloops →{ code: RECORD_LOCKED, message: …, httpStatus: 409 }. The hook's throw is measured in place (own properties[stack, message, code, statusCode],statusundefined).expected undefined to be 400 / 409 / 503); the undeclared-population pins stayed green, as they must.statusstamped unconditionally): the undeclared populations gainhttpStatus: 500and are caught — 2 pins inprotocol.batch-row-http-status.test.ts§3 and the real-driverSqliteErrorpin 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-approvalsgains avitest.config.tsaliasing@objectstack/metadata-protocolto source: the new integration test imports it as a value, andcheck:test-source-alias's prescribed fix for that is the alias, never a new entry in its shrink-only registry (check:test-source-aliasverified green).Generated by Claude Code