Repository navigation
fix(hono): createHonoApp's two catch arms answer a declared ADR-0112 envelope as declared, not as 500 (#23365) - #23530
Conversation
…pe rendering (#23365) Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…envelope through #16545's recogniser (#23365) Both arms read statusCode alone, so a throw declaring status and a registered code (a missing record's 404, the /data version refusals' 409 and 400, an authorization-store outage's 503) answered 500 INTERNAL_ERROR. They now ask declaredEnvelopeForThrow, exported from @objectstack/plugin-hono-server for this, and keep their unchanged answer for every other throw. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…ndering and its export (#23365) Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 60ddf2cb968abac15a6484ed365f15251308b4d0 && git checkout 60ddf2cb968abac15a6484ed365f15251308b4d0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 989aa483fd769d2849784c7552211be042d256b2 73e740fecec8df1c15f99703b26cbad94ce14528 && git checkout -B drift-repro 989aa483fd769d2849784c7552211be042d256b2 && git merge --no-ff 73e740fecec8df1c15f99703b26cbad94ce14528
node scripts/docs-audit/affected-docs.mjs --json 989aa483fd769d2849784c7552211be042d256b2
|
…ssified driver fault's declared 500 code included (#23365) Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
席位核对✅ ACCEPT: PR #23530 at
Checklist, read against GitHub and
Accepted deviations:
Out-of-scope findings, one line each:
Landing: this seat's Generated by Claude Code |
Contract reviewServed-tier: The face is a ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #23365
Clause-②: yes (widening): one added export of
@objectstack/plugin-hono-server,declaredEnvelopeForThrow, which was module-privateWhat this does
createHonoApp(@objectstack/hono) has two catch arms: the${authMount}/*mount's and the${prefix}/*catch-all's aroundHttpDispatcher.dispatch(). Both answerederrorJson(c, err.message || 'Internal Server Error', err.statusCode || 500), readingstatusCodealone. A throw that declaredstatusand a registeredcodetherefore lost both on this host and answered500 INTERNAL_ERROR.Both arms now go through one helper,
thrownJson. It asksdeclaredEnvelopeForThrow, the recogniser #16545 landed for@objectstack/plugin-hono-server's direct-mount routes, and renders the envelope it returns. When the recogniser answersundefined, the arm's old line runs unchanged. No second recogniser is written. The recogniser was module-private, so it is now exported from@objectstack/plugin-hono-server(itssrc/index.tsalready doesexport * from './adapter', soadapter.tsis the only file that changes there). That export is why line 2 readsClause-②: yes, against the claim'sno; the dispatch order names this exception. The seat re-grades.Readings on the wire (real
createHonoAppover the real dispatcher and protocol, in-memory SQLite)Taken at
167cebe824(fix) against84672ff876(pins only, adapter unchanged). Both are on this branch, and both sit onorigin/main869a381dc9.GET/PATCH/DELETE /data/task/no_such_row(corerecordNotFoundError, via the protocol)500 INTERNAL_ERROR"Record no_such_row not found in task"404 RECORD_NOT_FOUNDPATCHwith a staleIf-Match(protocolConcurrentUpdateError)500 INTERNAL_ERROR409 CONCURRENT_UPDATE, row unchangedDELETEwith a staleIf-Match500 INTERNAL_ERROR409 CONCURRENT_UPDATE, row keptPATCH/DELETEwithIf-Match: ""(protocolMalformedVersionTokenError)500 INTERNAL_ERROR400 VALIDATION_FAILED, row untouchedAuthzStoreUnavailableError('auth_gate'), raised byenforceAuthGateabovedispatch()'stry)500 INTERNAL_ERROR503 SERVICE_UNAVAILABLE, no cause in the bodyhandleRequestthrowsAuthzStoreUnavailableError500 INTERNAL_ERROR503 SERVICE_UNAVAILABLE, no cause in the bodyError(with acause) on either arm500 INTERNAL_ERROR, keyscodeandmessagestatusCode: 503and no code (callData's shape)503 SERVICE_UNAVAILABLEMechanism hypotheses, as measured
869a381dc9. Both lines wereindex.ts:782(auth) and:831(catch-all) and readstatusCodeonly.declaredEnvelopeForThrowreadsresolveThrownHttpError. ItsdeclaredStatuscomes from.status, then.statusCode, then the 400 that a validation-shaped throw declares. It requires that status to be a key ofHttpStatusErrorCodeMap, and the producer's own.codeto parse asErrorCode. It renders{ success: false, error: { code, message, details?, userMessage? } }, and on 5xx it applieslooksLikeInternalErrorLeakto the message.@objectstack/honocould not reach it through any existing export, so the export was added.currentVersionorcurrentRecord. The after-fix body is{"success":false,"error":{"code":"CONCURRENT_UPDATE","message":"Record task/ID was modified by another user (current version T1, expected T0)"}}. The recogniser forwards only thedetailsthat a producer declares (non-stringcode,issues, validationfields).ConcurrentUpdateErrorkeeps the two fields as own properties, and onlyRestServer's classification arm reads them (packages/types/src/data-error-classification.ts, theCONCURRENT_UPDATEarm). Per the ruling, the recogniser is not widened here. This is reported to the seat as a finding.Response: anAPIErrorbecomes its ownResponse, and anything else becomes a bare500. The dispatcher'shandleAuthRequestcatches the auth service's throw itself. The arm takes the same rendering for uniformity: one rule, not two. Its pin uses a host-supplied auth service whosehandleRequestthrows."No cause in the body"
Read as: the body of a non-envelope throw gains nothing. It gets no
cause, nodetailsand nouserMessage; it carriescodeandmessageonly. Each control row asserts exactly that, with a cause whose text must not appear in the body. The old fallback line is unchanged byte for byte, so a bareError's own message is still in its500body, as it was before.packages/adapters/hono/src/hono.test.tsalready pins that (returns error on dispatch exception), and this PR leaves that pin alone. Withholding the message of an undeclared 500 on this host would be a separate behaviour change; see the acceptance notes.Tests
packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts, 13 rows. Before the fix: 9 failed, 4 passed (the four controls). After: 13 passed.@objectstack/http-conformance: 13 files, 137 tests passed.typecheck(withcheck:test-typecheck) is green, andtsc --listFiles -p tsconfig.test.jsoncounts 13 of 13 test files, the new one included.@objectstack/hono: 7 files, 154 tests passed;typecheckgreen.@objectstack/plugin-hono-server: 29 files, 336 tests passed;typecheckgreen.Ablation (one leg per arm,
scripts/ablation-replace.mjsin wrap mode plus an outertrap … EXIT INT TERMrestore)In http-conformance's
vitest.config.ts, the subject@objectstack/honois aliased topackages/adapters/hono/src/index.ts, so a mutation of the source reaches the run without a build.3529a20128d4to51939c434b24. Result: 8 failed, 5 passed. All 8 catch-all declared pins went red. The controls and the two auth-arm rows stayed green.3529a20128d4to4314a9ba898d. Result: 1 failed, 12 passed. Only the auth-arm declared pin went red, and its bare-Errorcontrol stayed green.3529a20128d4) andgit diff HEADis empty.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 66 commands at287fd99bda. 65 exited 0.pnpm check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: it reads the whole tree'sdist/), which counts as NOT MEASURED locally; CI runs it.--ranreports 66 derived, 65 run, 1 NOT-MEASURED, 0 UNRUN.eslint --no-inline-configover the diff's three.tsfiles exited 0, linting 3 files with 0 errors and 0 warnings. Positive control: a stdin file inpackages/adapters/hono/src/importing a root namespace from@objectstack/specexits 1 (no-restricted-imports). Repo-widepnpm lintis CI's run.Changeset
.changeset/23365-hono-declared-envelope.md,minorfor both packages.@objectstack/plugin-hono-serverisminorbecauseClause-②: yes(an added export) takes at leastminor.@objectstack/honoisminorto match #16545's level: that change applied the same rule to the direct-mount door and shipped asminor(cefe068702). The wording follows that changeset's sections.Acceptance notes
looksLikeInternalErrorLeakfilter. It was already the case before this change, and it is unchanged here. Noted only; not filed.looksLikeInternalErrorLeakfires. The dispatcher's own exit withholds the message of every declared 5xx (commit 0783d7b). The two Hono doors agree with each other by construction, but not with that exit. Noted only.maingained989aa483fdand0be162c75dafter the base. Neither touchespackages/adapters/hono,plugin-hono-server,http-conformance, the dispatcher or@objectstack/types, andgit merge-treeis clean, so the branch was not merged; the merge queue rebuilds onmain.Generated by Claude Code