Skip to content

hono adapter: createHonoApp's catch-all renders an error thrown out of dispatch() from statusCode alone, so a declared status/code (a missing record's 404 RECORD_NOT_FOUND, an outage's 503) answers 500 INTERNAL_ERROR #23365

Closed

Description

@objectstack-fleet

Filing gate ①, class (b): a product defect with a named landing point, and its reach was measured once at a public door.

reach: measured by #23225 slice 2's dev at 90174d8bf (PR #23364's report, out_of_scope_findings). On a host that fronts HttpDispatcher.dispatch() with @objectstack/hono's createHonoApp catch-all, over a booted showcase, a signed-in member's GET, PATCH or DELETE of /data/:object/:id for an id that names no row answers 500 INTERNAL_ERROR. The message reads "Record … not found in …". The standalone REST door answers the same request 404 RECORD_NOT_FOUND.

Filed by domain:cli seat 1 (#6024) · huangyiirene · session session_015sSBzUJfLxTo3ZZ68LLfCG, from the report. ⛔ Not a claim. Lane and grade are triage's.

Reader who acts: triage grades it. The landing is packages/adapters/hono/src/index.ts, which is domain:cli by the lane table.

What the source says (main 6d96721f)

Not a duplicate of

What done looks like

Dedupe

MCP search_issues on objectstack-ai/objectstack, closed included:


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p2 · domain:cli · area:api, into pm:queue (finding removed). createHonoApp's catch arms render a declared ADR-0112 status/code, as #16545 made the direct-mount adapter do

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-14T02:57Z. ⛔ Not a claim, ⛔ not a dispatch.

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    and removed on Oct 14, 2026
  3. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 7, serial (#23365: createHonoApp's two catch arms render an escaped throw that carries a declared ADR-0112 status and a registered code as that envelope, through #16545's recogniser; a bare throw stays 500) · 2026-10-14T17:51Z
    Session: session_01Q9PZ5Pzqcus4woVvTZwWNU
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23365-hono-declared-envelope
    Worktree: objectstack-issue-23365
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    Scope: the maintainer's 「当前任务处理完,后续改为串行派发」, the one serial claim. This seat's previous claim, #23347 (readings), was released to triage with pm:retriage (6174333277, then the Release on that card). #23365 is next in the lane's order; the target:v18 p2 cards are still behind PR #23452.
    File surface (read on origin/main 869a381dc9; stop on a breach and explain it in the report):

    • packages/adapters/hono/src/index.ts, ONLY createHonoApp's two catch arms. Both read errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500): :782 (the auth fallback) and :831 (the dispatch() catch-all).
    • packages/plugins/plugin-hono-server/src/adapter.ts and its entry src/index.ts, ONLY if reusing declaredEnvelopeForThrow (adapter.ts:284, module-private today) needs it exported. In that case:
      • the PR declares the added export Clause-②: yes;
      • this seat re-grades the claim, and a TIER_DEFAULT seat record is owed.
      • ⛔ No second recogniser.
    • One new pin file, packages/qa/http-conformance/src/hono-*.conformance.test.ts. @objectstack/hono's own tests alias the runtime to a stub, so the wire pins live where the real pair is composed. Plus one .changeset/23365-*.md.
    • ⛔ Not packages/runtime, not packages/core, not packages/spec, not the direct-mount adapter's own behaviour.

    Container & model: S, judged (two catch arms and one reuse; the recogniser exists), mode:subagent, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). Ceiling as graded: Clause-②: no (the adapter renders what the contract already declares, triage 6159864250), no contract face, so CI plus this seat's read. If an export is added, TIER_DEFAULT (the declaration face).
    Clause-②: no
    Responsibility: createHonoApp's two catch arms in packages/adapters/hono (this lane) | plugin-hono-server's #16545 recogniser, reused, at most exported | every createHonoApp host whose dispatch() throws a declared envelope (a missing record's 404, an outage's 503, the /data version refusals' 409 and 400)
    Thread-read: 6159864250
    Serial constraints cleared:


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23365,
    "status": "done",
    "branch": "claude/issue-23365-hono-declared-envelope",
    "pr": "#23530",
    "session": "session_01Q9PZ5Pzqcus4woVvTZwWNU (this run's harness-stamped id; subagent, so it is the parent's)",
    "premise_still_valid": true,
    "summary": "Both of createHonoApp's catch arms (packages/adapters/hono/src/index.ts: the auth mount's, at :782 on 869a381, and the dispatch() catch-all's, at :831) now go through one helper, thrownJson. It asks declaredEnvelopeForThrow, #16545's recogniser, and renders the envelope it returns. When the recogniser answers undefined, the arm's old errorJson(c, err.message || 'Internal Server Error', err.statusCode || 500) line runs, byte-identical. No second recogniser was written. The recogniser was module-private, so it is now exported from @objectstack/plugin-hono-server: adapter.ts only, since src/index.ts already does export * from './adapter'. That is the dispatch's named exception, so PR line 2 reads Clause-②: yes (widening) and the seat re-grades. Wire readings on a real createHonoApp over the real dispatcher and protocol (in-memory SQLite). Before (adapter unchanged, 84672ff): every declared row answered 500 INTERNAL_ERROR. After (167cebe): GET, PATCH and DELETE of a missing record answer 404 RECORD_NOT_FOUND. A stale If-Match on PATCH or DELETE answers 409 CONCURRENT_UPDATE, with the row unchanged. If-Match: "" on PATCH or DELETE answers 400 VALIDATION_FAILED, with the row untouched. AuthzStoreUnavailableError('auth_gate'), raised by enforceAuthGate above dispatch()'s try, answers 503 SERVICE_UNAVAILABLE with no cause in the body. On the auth mount, an auth service's handleRequest throwing AuthzStoreUnavailableError answers 503. A bare Error still answers 500 INTERNAL_ERROR on both arms, with keys code and message only and no cause. A statusCode-only throw keeps its status. (d) The rendered 409 carries neither currentVersion nor currentRecord; this is reported below as a finding, and the recogniser was not widened. (e) No in-repo producer reaches the auth arm with a declared envelope: better-call 1.4.0's router turns a handler throw into a Response, and the dispatcher's handleAuthRequest catches the auth service's throw. The arm takes the same rendering for uniformity, one rule, not two. Extra measured blast radius, stated in the changeset (73e740f): a driver fault the engine already classified keeps its 500 and now carries its declared code. GET /data/:object over a missing table answers 500 DATABASE_ERROR, with the same message as before; it answered 500 INTERNAL_ERROR before this change.",
    "tests": "New: packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts, 13 rows. BEFORE (84672ff, adapter unchanged): Tests 9 failed | 4 passed (13). Each declared row read e.g. {\"success\":false,\"error\":{\"code\":\"INTERNAL_ERROR\",\"message\":\"Record no_such_row not found in task\"}}: expected { status: 500 … } to deeply equal { status: 404 … }. AFTER (167cebe): Tests 13 passed (13). Full suites, all run through os-verify-lock (VERDICT command-exit 0): @objectstack/http-conformance Test Files 13 passed (13), Tests 137 passed (137); its typecheck plus check:test-typecheck OK, and tsc --listFiles -p tsconfig.test.json counts 13 of 13 test files, the new one included. @objectstack/hono Test Files 7 passed (7), Tests 154 passed (154), typecheck exit 0. @objectstack/plugin-hono-server Test Files 29 passed (29), Tests 336 passed (336) (TEST_EXIT=0), typecheck TC_EXIT=0. ABLATION, one leg per arm, via node scripts/ablation-replace.mjs in wrap mode plus an outer trap restore (EXIT INT TERM) on the absolute path. The subject @objectstack/hono is vitest-aliased to packages/adapters/hono/src/index.ts in http-conformance's vitest.config.ts, so a mutation of the source reaches the run without a build or a dist preflight. Catch-all leg (arm reverted to the old line): anchor x1 to x0, blob 3529a20128d4 to 51939c434b24, Tests 8 failed | 5 passed (13). All 8 catch-all declared pins went red; the controls and both auth-arm rows stayed green. Auth leg: anchor x1 to x0, blob 3529a20128d4 to 4314a9ba898d, Tests 1 failed | 12 passed (13). Only the auth-arm declared pin went red, and its bare-Error control stayed green. Restore for each leg: blob == HEAD (3529a20128d4) and git diff HEAD is empty, plus the outer trap's hash check. The head moved to 73e740f after these runs by a changeset-only commit (no .ts blob changed), so the test readings stand for that head's code.",
    "gates": "LOCAL, at the final head 73e740f: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 66 commands (the list is identical to 287fd99's). Each was run with its exit code captured before any pipe. --ran: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). The earlier union at 287fd99 was 65 exit 0 plus pnpm check:dual-build-cjs-loads :: exit 3 (PREREQUISITE NOT MET: the whole tree had no dist/). That gate exits 0 on the 73e740f run. Lint, narrowed (the dispatch's pnpm lint or a proven narrowing). Three pieces of evidence: (1) the file set is the diff's .ts files against BASE 869a381 (3 files); (2) eslint --no-inline-config --format json reports 3 files linted, 0 errors, 0 warnings, exit 0; (3) a positive control, stdin as packages/adapters/hono/src/__lint_control.ts importing Data from '@objectstack/spec', exits 1 with no-restricted-imports. CI check-runs on 73e740f at report time: Lint & Repo Gates success; TypeScript Type Check success; Build Core success; Governed Surface Queue Guard success; Lint · ESLint success (the repo-wide pnpm lint); Check Changeset success; Dogfood Regression Gate success; Type Check · workspace/source/consumer/debt ledger success; Test Core 1/6, 4/6, 5/6 and 6/6 success; Test Core 2/6 and 3/6 in_progress. The 287fd99 run shows TypeScript Type Check and Lint & Repo Gates as failure. Those are aggregates whose 17 sub-jobs were cancelled by the 73e740f push, not a measured red.",
    "mcp_calls": "0",
    "api_writes": "3 writes executed as objectstack-fleet[bot] through 2 relay strokes, each a POST /repos/objectstack-ai/objectstack/dispatches sent by scripts/pm/fleet-write/dispatch.mjs. Stroke 1 (pr_create): POST /repos/objectstack-ai/objectstack/pulls (#23530, draft, body read back byte-identical at 8791 bytes) and POST /repos/objectstack-ai/objectstack/issues/23530/assignees (sunhaolin). Stroke 2 (comment): POST /repos/objectstack-ai/objectstack/issues/23365/comments (this report). Plus git push ×5 (not REST). Label writes: 0, since the dispatch named none and skip-changeset does not apply.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: b · reach: PATCH and DELETE /api/v1/data/task/ID with a stale If-Match through a real createHonoApp host (this PR's conformance rig, at 167cebe and later) answer 409 {\"success\":false,\"error\":{\"code\":\"CONCURRENT_UPDATE\",\"message\":\"Record task/ID was modified by another user (current version T1, expected T0)\"}}, with no currentVersion or currentRecord. · contract text: content/docs/api/wire-format.mdx, section 'Concurrent Update — 409 Conflict', carries currentVersion; ConcurrentUpdateError's docblock (packages/metadata-protocol/src/protocol.ts) says the HTTP layer 'includes both the current server-side version and the current record payload'. · Seam: spec: none (contract: wire-format.mdx 409 CONCURRENT_UPDATE, ConcurrentUpdateError.currentVersion/currentRecord) → runtime: declaredEnvelopeForThrow (packages/plugins/plugin-hono-server/src/adapter.ts), which forwards only resolveThrownHttpError's details (non-string code, issues, validation fields), reached by createHonoApp's thrownJson and by HonoHttpServer.wrap/onError | renderer: objectui packages/plugin-detail/src/InlineEditSaveBar.tsx:292-301, which reads err.currentRecord and err.currentVersion. · Only RestServer's classification arm renders them (packages/types/src/data-error-classification.ts, the CONCURRENT_UPDATE arm). · The dispatch named this card as that gap's carrier; per the ruling it is not carried here. · dedupe words: CONCURRENT_UPDATE currentVersion currentRecord 409 createHonoApp declaredEnvelopeForThrow conflict body",
    "class: b · reach: POST /api/v1/data/ghost through a real createHonoApp host, where object ghost is registered and its table was never synced (probe in this PR's rig, after the fix, on the unchanged fallback line), answers 500 {\"success\":false,\"error\":{\"code\":\"INTERNAL_ERROR\",\"message\":\"insert into [statement and bound values redacted] - no such table: ghost\"}}. The backend's own diagnostic reaches the client: an undeclared 5xx on either createHonoApp arm ships err.message with no looksLikeInternalErrorLeak filter. · contract text: the disclosure filter 'every door emitting a thrown message runs' (HttpDispatcher.error, packages/rest's registrars, and declaredEnvelopeForThrow's own docblock); the same file's /auth/config arm applies it ('[#3867 follow-up] Same guard the two dispatcher error exits apply'). · The driver already redacts the statement and values, so what leaks is the SQLite diagnostic and the table name. Pre-existing and unchanged by this PR. · Seam: spec: none (contract: looksLikeInternalErrorLeak, @objectstack/types) → runtime: packages/adapters/hono/src/index.ts thrownJson fallback errorJson(c, err.message …) | renderer: none · dedupe words: createHonoApp undeclared 500 thrown message looksLikeInternalErrorLeak driver diagnostic catch-all disclosure",
    "carrier: none · noted, not filed (also in the PR's Acceptance notes): for a DECLARED 5xx, the shared recogniser keeps the message unless looksLikeInternalErrorLeak fires, while the dispatcher's own exit withholds the message of every declared 5xx (commit 0783d7b). The two Hono doors agree with each other but not with that exit."
    ],
    "deviations": [
    "Clause-② moved from the claim's no to yes (widening). This is the dispatch's named exception: the recogniser had to be exported. The seat re-grades and owes the TIER_DEFAULT record.",
    "The changeset was edited after the PR opened (73e740f) to name the rule's other limbs, which a probe measured: a classified driver fault now answers 500 DATABASE_ERROR, not INTERNAL_ERROR. The PR body was written once and not PATCHed. Its Gates section cites the 287fd99 union (65 run, 1 NOT-MEASURED), and it lacks the DATABASE_ERROR sentence. The seat may replace the Gates bullet with 66 derived, 66 run, 0 NOT-MEASURED at 73e740fece and add that sentence under Readings.",
    "'No cause in the body' was executed as follows: the body of a non-envelope throw gains nothing (no cause, details or userMessage; only code and message), and each control asserts that against a cause whose text must not appear. The fallback line is byte-identical, so a bare Error's own message stays in its 500 body, as packages/adapters/hono/src/hono.test.ts already pins. The PM's hypothesis (c), a bare Error 'with no cause in the body', holds under this reading. If the triage meant the thrown message itself, that would be a separate behaviour change, not made here.",
    "The auth-arm declared pin uses a host-supplied auth service whose handleRequest throws, because no in-repo producer reaches that arm (hypothesis e). The two fallback controls on the catch-all spy on protocol.getData. Every other declared row uses the real producer.",
    "Lint conflict, noted: the dispatch says this lane always adds pnpm lint, while the role file says repo-wide scans are CI's. I ran the dispatch's own alternative, the proven narrowing with three pieces of evidence, and CI's Lint · ESLint is green on 73e740f.",
    "Two temporary probe files (zz-probe-23365 and zz-probe-leak-23365) were created in packages/qa/http-conformance/src to read wire bodies, and deleted before any gate run or commit. git status was clean afterwards.",
    "origin/main gained 989aa48 and 0be162c after BASE. Neither touches the diff's packages, the dispatcher or @objectstack/types, and merge-tree is clean, so the branch was not merged."
    ],
    "files_changed": [
    "packages/adapters/hono/src/index.ts",
    "packages/plugins/plugin-hono-server/src/adapter.ts",
    "packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts",
    ".changeset/23365-hono-declared-envelope.md"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23530 comment 6176144207 · 2026-10-14T19:07Z

    Contract review (TIER_DEFAULT, this seat, the PR's Clause-②: yes declaration face): PR #23530 comment 6176155963, PASS on 73e740fece.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23530 → e1cefa58e3 · 2026-10-14T19:38Z

    domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU · holder of claim 6174741572, released by this landing. Review: seat record 6176144207 and this seat's TIER_DEFAULT contract record 6176155963 (PASS on 73e740fece; the face is the PR's Clause-②: yes, one added export of @objectstack/plugin-hono-server).

    Reading Value
    PR merged: true, merged_at 2026-10-14T19:36Z, through the merge queue (added_to_merge_queue 2026-10-14T19:11Z)
    Queue branch gh-readonly-queue/main/pr-23530-* absent (git ls-remote, 0 refs)
    origin/main e1cefa58e3 is an ancestor; one parent (b1cfa3d263); 4 files, +369 / −3, as reviewed
    Closing keywords in the landed commit Fixes #23365 only
    Lane reconciliation 15 open domain:cli cards. Against the 17:28Z set: minus #23365 (this landing), #23347 (re-routed to domain:services by triage), and #15213 and #23398 (closed completed, outside this seat). #23531 and #23532 (filed at this PR's ACCEPT) are still finding, ungraded

    State change: #23365 closed completed by the merge; pm:dispatched removed in this act. Every createHonoApp host now answers a declared ADR-0112 envelope as declared, including the /data version refusals this seat's PR #23460, PR #23490 and PR #23515 shipped. Those changesets' "still renders 500" caveat no longer holds from this release. #23531 and #23532 are ordered after this PR, and that condition now holds.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:clipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions