Uh oh!
There was an error while loading. Please reload this page.
fix(api): envelope the plugin-mounted Hono error paths — six refusal bodies stop speaking the pre-#3675 dialect - #9456
Conversation
Six hand-built refusal bodies on plugin-mounted Hono routes departed from BaseResponseSchema, invisible to every check in the repo until #9267 added the route-envelope gate's third surface. This converts the error-path half: - plugin-hono-server/src/adapter.ts — the 404, 405, and two 500 refusals now answer { success: false, error: { code, message } }; the 405's method/path/ allowed move into error.details and its METHOD_NOT_ALLOWED code moves from beside `error` to inside it (wire value unchanged). - adapters/hono/src/index.ts — errorJson wrote the HTTP status into error.code; it now derives the ADR-0112 member from the status through resolveThrownHttpError, the rule the REST and dispatcher doors already read. - cli/src/commands/serve.ts — the unbound-hostname 404 drops its bare-string error and two stray top-level keys. qa/http-conformance's NodeHttpServer mirrors the adapter's unmatched-request bodies byte-for-byte by design and is locked to them cross-adapter by fallback-seam.conformance.test.ts, so it moves in the same commit. check-route-envelope.mjs counters banked: adapter.ts and serve.ts to conformant, adapters/hono keeps unenveloped 2 (retargeted to #9436). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WKSnqAaoqtW3QX7SSf1Vk
📓 Docs Drift CheckThis PR changes 4 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop):
|
…lope (#9364) packages/runtime drives the hono adapter through the dispatcher plugin, and its integration tests pin the adapter's refusal bodies. Those pins still asserted the pre-conversion shapes. Adapter pins, moved — verified as adapter pins rather than another door's contract by reading what each case exists to prove: - dispatcher-plugin.multi-tenant-endpoint (2 sites) and dispatcher-plugin.ready — both assert "the transport's own 404 stands", one as the decline branch of environment resolution, one as an explicit anti-vacuity control that the harness mirrors prod. - dispatcher-plugin.endpoint-fallback — BARE_NOT_FOUND renamed TRANSPORT_NOT_FOUND (the name described the old shape) across 6 uses, plus the 405 case whose code/allowed moved into error / error.details. - mcp-skill-method-not-allowed — the boundary case now reads error.details.allowed, asserted by value (the four-verb set #7649's own fix produces) rather than by presence.⚠️ The conversion also drained the POST/DELETE cases in that last file of their discriminating power without failing: they separated the two doors by `typeof body.error !== 'string'` and three top-level not.toHaveProperty checks, all four of which became true of the ADAPTER answer too once it gained the same envelope. Replaced with the distinction that still holds — the domain fills error.httpStatus and no details, the adapter the reverse — so the suite still fails in the world it exists to catch. Assertions are at least as strong as before; none weakened or skipped, no ratchet touched. check-route-envelope counters unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WKSnqAaoqtW3QX7SSf1Vk
…gin-route-envelope-error-paths # Conflicts: # scripts/check-route-envelope.mjs
…gin-route-envelope-error-paths
…gin-route-envelope-error-paths
Heads-up before you merge — a new head is on its way, and the current one cannot satisfy the required checks. I saw the The one thing worth knowing before you hit merge: this PR's current head The dev on this card has already merged So: either wait for that push and merge the new head, or tell me and I'll have it pushed immediately. Merging is yours; I'm only flagging that the head you approved cannot currently pass. Context on the change itself, if useful: the card converts the plugin-mounted Hono error paths onto the declared envelope — 11 gate counters removed, none raised, post-merge gate summary Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9364
Converts the error-path half of what #9267's third gate surface measured — the plugin-mounted Hono refusals that departed from
BaseResponseSchema.All gate results below are from
e2ca3b1ea, the merge oforigin/maininto this branch. That merge matters: #9455 landed #9389's ruledexemptentries in the same gate script (+351/−69), so the numbers here are the post-merge ones.Per-file counter deltas
node scripts/check-route-envelope.mjs, before and after — the acceptance instrument for this card:packages/plugins/plugin-hono-server/src/adapter.tsunenveloped 4,stringError 4,siblingCode 1{})packages/cli/src/commands/serve.tsunenveloped 1,stringError 1{})packages/adapters/hono/src/index.tsunenveloped 2,errorCodeNotString 1unenveloped 2(unchanged), retargeted to #9436Eleven counters removed, none raised, nothing rebaselined and no
exemptentry added by me. Post-merge summary line:The three
exemptare #9455's, untouched and still closed at their exact ruled counts —runtime-config-plugin.tsatunenveloped 1,current-user-endpoints.tsat9,auth-plugin.tsat3. The one remainingratchetisadapters/hono.What changed on the wire
@objectstack/plugin-hono-server— the adapter's refusals, i.e. what any host using it as its transport answers for an unmatched request or a handler that produced nothing:{ error: 'Not found' }{ success: false, error: { code: 'ENDPOINT_NOT_FOUND', message: 'Not found' } }{ error, code, message, method, path, allowed }{ success: false, error: { code: 'METHOD_NOT_ALLOWED', message, details: { method, path, allowed } } }{ error: 'No response from handler' }{ success: false, error: { code: 'INTERNAL_ERROR', message: … } }{ error: 'Fallback handler failed' }{ success: false, error: { code: 'INTERNAL_ERROR', message: … } }@objectstack/hono—errorJsonwrote the HTTP status intoerror.code, whereApiErrorSchema.codedeclares a closed string vocabulary. It now derives the standard member from the status viaresolveThrownHttpError(@objectstack/types) — the rule the REST and dispatcher doors already read (ADR-0112, #9106), so this third door does not become a fourth dialect.@objectstack/cli— theOS_ROOT_DOMAINunbound-hostname 404 loses its bare-string error and two stray top-level keys;hostnamemoves toerror.details. TheAccept: text/htmlbranch still serves the styled 404 page.All codes come from the
StandardErrorCodecatalog (ENDPOINT_NOT_FOUND,METHOD_NOT_ALLOWED,INTERNAL_ERROR,RESOURCE_NOT_FOUND) or are already-registered ledger members (ENVIRONMENT_NOT_FOUND), so nopackages/specedit and no new registration was needed. The 405'scodevalue is unchanged — only its position moved, from besideerrorto inside it.Dialect choice, verified rather than assumed
Per the #9326 / PR #9425 precedent: the ADR-0112 nested shape was chosen and then measured. Two traps were live here and both were checked against the scanner rather than reasoned about:
success: falsewhile leavingmethod/path/allowed/hostnameat the top level would have moved those bodies fromunenvelopedintostrayKeys— a net-zero trade, not a fix. They went intoerror.details, whichApiErrorSchemadeclares for exactly this.strayKeysstayed at 0 on all three files.errorJson, renaming the third parameter fromcodetostatusis load-bearing, not cosmetic: the gate flags anerror.codeshorthand whose identifier is the same one passed as the status argument, so keeping the old name would have lefterrorCodeNotStringalive with the value already fixed.The literals are deliberately written inline at each
c.json(...)rather than hoisted into shared constants. The gate judges the object literal; an identifier reads to it as a relayed body it must not police — hoisting would have zeroed these counters by hiding the bodies from the scanner instead of by conforming them.The merge with #9455, resolved by re-deriving
Both sides edit
PLUGIN_ROUTE_MODULES. Rather than pick hunks — where choosing a side silently restores a counter this card removed, or drops a ruled exemption that is not mine to drop — the file was reset to main's version whole (git checkout origin/main -- …) and the three conversions re-applied onto it by exact anchor. Verified afterwards in both directions: all three of #9455'sexemptentries present with their exact numbers, and all three of my conversions present.One line of #9455's new header needed updating rather than preserving: it named
adapter.ts,adapters/hono/src/index.tsandserve.tsas ordinary doors that "stay tracked drift under #9364". Post-merge two are conformant and the third is retargeted, so that sentence now describes the contrast instead — the ruling still does not reach them, and #9364 converted them, which is what a ratchet is for.The three
ratchet: '#9364 …'pointers flagged as a possible dangling carry-forward were checked on the merged ref: after #9455's restructuring they named this PR's own three entries, not the bare-payload files. All three are gone now — converted or retargeted — so the carry-forward was entirely mine and is entirely closed. The only#9364strings left in the script are inside #9455's own synthetic self-test fixtures (x.ts/y.ts), which are test data, not ledger entries.Consumer packages, and two declared-surface widenings
Both are mechanically forced by the conversion rather than chosen.
packages/qa/http-conformance— that package's zero-dependencyNodeHttpServermirrors the adapter's unmatched-request bodies byte-for-byte on purpose (its source says "Same body as the primary adapter" and "Both paths must produce the byte-identical answer"), andfallback-seam.conformance.test.tsruns the same cases against both adapters. Converting only the primary adapter would have made "both adapters agree" false in the suite that exists to assert it. The file is invisible to the route-envelope gate — it writes vianodeRes.end(JSON.stringify(...)), notc.json(...)— so no counter moves because of it.Its two remaining bare 500 bodies (a handler throw, and the SSE-teardown 500) are left alone: they mirror nothing in the primary adapter, no conformance case asserts them, and no counter covers them. Fixing them would have been unbounded creep with no evidence pinning the shape.
packages/runtime— authorised by the PM afterTest Corewent red on the first head. That package drives the hono adapter through the dispatcher plugin, and four of its integration suites pin the adapter's refusal bodies. I confirmed these are adapter pins, not another door's contract, by reading what each case exists to prove: the twomulti-tenant-endpointcases assert "the transport's own 404 stands" as the environment-resolution decline branch;readyasserts it as an explicit anti-vacuity control that the harness mirrors prod;endpoint-fallback's shared constant is documented as "the transport's unmatched answer"; and themcp-skillcase is labelled the boundary probe forunmatchedResponse()being the correct owner of an unmounted verb. So the conversion did not over-reach — these pins had to move with it.typeof body.error !== 'string'plus three top-levelnot.toHaveProperty('method'/'path'/'allowed')checks. Once the adapter gained the same envelope, all four became true of the adapter answer too — so those assertions kept passing while no longer discriminating, in exactly the world the file exists to catch. They are replaced with the distinction that still holds: the domain branch fillserror.httpStatusand nodetails, the adapter the reverse. Every touched assertion is at least as strong as before; none weakened, skipped or quarantined.Doors that legitimately did not move, checked and left alone:
packages/rest's 405 (its ownOBJECT_API_METHOD_NOT_ALLOWEDcode),runtime'serror-envelope.conformance.test.tsandmeta-verb-fallthrough.test.ts(dispatcher called directly, no adapter in the path), andpackages/core's QA-harness fetch mocks — whose sibling row uses{ error: 'nope' }, proving the body there is an arbitrary payload and the assertion is on the status.Verification
Full rebuild after the merge (
pnpm install --frozen-lockfile, 70/70 turbo build tasks,packages/runtime/.objectstackcleared), then tests one2ca3b1ea:pnpm --filter @objectstack/runtime test— 167 files, 2503 tests passed (main brought 4 new ones)pnpm --filter @objectstack/plugin-hono-server test— 18 files, 211 tests passedpnpm --filter @objectstack/http-conformance test— 4 files, 72 tests passed (the cross-adapter suite, both adapters over real sockets)pnpm --filter @objectstack/hono test— 2 files, 73 tests passedpnpm --filter @objectstack/cli test— 131 files, 1412 tests passedRefusal cases assert
error.codeandstatus, per the dispatch rule — includingerror.details.method/.path/.allowedon the 405, asserted by value rather than by presence.Reverse verification (run pre-merge, on the committed tree) — reverting just the 404 body to
{ error: 'Not found' }turned the gate red in the predicted direction, proving the banked zeros are load-bearing pins and not vacuous:Gate union, all green on
e2ca3b1ea:check:route-envelope,check:cross-package-test-inputs,check:test-source-alias,check:type-source-resolution,check:changeset-gate-self-tests,check:objectui-changeset,check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:type-check-coverage,check:type-check-debt(--re-measure, on the rebuilt closure: 33 entries, 1926 raw errors, none above its recorded number), pluscheck-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check-cross-package-test-inputs,check-nul-bytes,check-affected-docs.Out-of-scope findings filed
{ data }discovery bodies lose their ratchet owner when #9364 lands #9436 — the two bare{ data }discovery bodies inadapters/honolose their ratchet owner when this lands (Blocked-by: #9389). This PR retargets that entry's pointer to it, so no ledger entry names a closed card.os serve's unknown-hostname guard has no test seam — the whole middleware, refusal body included, is unreachable from a test #9442 —os serve's unknown-hostname guard has no test seam: the whole middleware is built inside a closure inrun()and is unreachable from a test. Pre-existing and unchanged by this PR in either direction, but the health/readiness bypass it contains is a "silently stops working" branch with no regression test. Extracting it is a refactor with real judgment calls, not a mechanical fix, so it was filed rather than ridden in here.Generated by Claude Code