Repository navigation
Commit e1cefa5
Fixes #23365
Clause-②: yes (widening): one added export of
`@objectstack/plugin-hono-server`, `declaredEnvelopeForThrow`, which was
module-private
## What this does
`createHonoApp` (`@objectstack/hono`) has two catch arms: the
`${authMount}/*` mount's and the `${prefix}/*` catch-all's around
`HttpDispatcher.dispatch()`. Both answered `errorJson(c, err.message ||
'Internal Server Error', err.statusCode || 500)`, reading `statusCode`
alone. A throw that declared `status` and a registered `code` therefore
lost both on this host and answered `500 INTERNAL_ERROR`.
Both arms now go through one helper, `thrownJson`. It asks
`declaredEnvelopeForThrow`, the recogniser #16545 landed for
`@objectstack/plugin-hono-server`'s direct-mount routes, and renders the
envelope it returns. When the recogniser answers `undefined`, 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` (its `src/index.ts` already does
`export * from './adapter'`, so `adapter.ts` is the only file that
changes there). That export is why line 2 reads `Clause-②: yes`, against
the claim's `no`; the dispatch order names this exception. **The seat
re-grades.**
## Readings on the wire (real `createHonoApp` over the real dispatcher
and protocol, in-memory SQLite)
Taken at `167cebe824` (fix) against `84672ff876` (pins only, adapter
unchanged). Both are on this branch, and both sit on `origin/main`
`869a381dc9`.
| Row (producer) | Before | After |
|---|---|---|
| `GET` / `PATCH` / `DELETE /data/task/no_such_row` (core
`recordNotFoundError`, via the protocol) | `500 INTERNAL_ERROR` "Record
no_such_row not found in task" | `404 RECORD_NOT_FOUND` |
| `PATCH` with a stale `If-Match` (protocol `ConcurrentUpdateError`) |
`500 INTERNAL_ERROR` | `409 CONCURRENT_UPDATE`, row unchanged |
| `DELETE` with a stale `If-Match` | `500 INTERNAL_ERROR` | `409
CONCURRENT_UPDATE`, row kept |
| `PATCH` / `DELETE` with `If-Match: ""` (protocol
`MalformedVersionTokenError`) | `500 INTERNAL_ERROR` | `400
VALIDATION_FAILED`, row untouched |
| The ADR-0069 gate's session read fails
(`AuthzStoreUnavailableError('auth_gate')`, raised by `enforceAuthGate`
above `dispatch()`'s `try`) | `500 INTERNAL_ERROR` | `503
SERVICE_UNAVAILABLE`, no cause in the body |
| Auth mount: the auth service's `handleRequest` throws
`AuthzStoreUnavailableError` | `500 INTERNAL_ERROR` | `503
SERVICE_UNAVAILABLE`, no cause in the body |
| Control: a bare `Error` (with a `cause`) on either arm | `500
INTERNAL_ERROR`, keys `code` and `message` | unchanged |
| Control: a throw that declares `statusCode: 503` and no code
(`callData`'s shape) | `503 SERVICE_UNAVAILABLE` | unchanged |
## Mechanism hypotheses, as measured
- **(a) The two arms.** Confirmed on `869a381dc9`. Both lines were
`index.ts:782` (auth) and `:831` (catch-all) and read `statusCode` only.
- **(b) The recogniser.** `declaredEnvelopeForThrow` reads
`resolveThrownHttpError`. Its `declaredStatus` comes from `.status`,
then `.statusCode`, then the 400 that a validation-shaped throw
declares. It requires that status to be a key of
`HttpStatusErrorCodeMap`, and the producer's own `.code` to parse as
`ErrorCode`. It renders `{ success: false, error: { code, message,
details?, userMessage? } }`, and on 5xx it applies
`looksLikeInternalErrorLeak` to the message. `@objectstack/hono` could
not reach it through any existing export, so the export was added.
- **(c) Declared throws.** All five measured; see the table.
- **(d) The 409 body.** It does **not** carry `currentVersion` or
`currentRecord`. 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 the `details` that a producer
declares (non-string `code`, `issues`, validation `fields`).
`ConcurrentUpdateError` keeps the two fields as own properties, and only
`RestServer`'s classification arm reads them
(`packages/types/src/data-error-classification.ts`, the
`CONCURRENT_UPDATE` arm). Per the ruling, the recogniser is not widened
here. This is reported to the seat as a finding.
- **(e) The auth-mount arm.** No in-repo producer was found that reaches
it with a declared envelope. better-call 1.4.0's router (the one
better-auth's handler runs) turns a handler throw into a `Response`: an
`APIError` becomes its own `Response`, and anything else becomes a bare
`500`. The dispatcher's `handleAuthRequest` catches 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 whose `handleRequest`
throws.
## "No cause in the body"
Read as: the body of a non-envelope throw gains nothing. It gets no
`cause`, no `details` and no `userMessage`; it carries `code` and
`message` only. 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 bare `Error`'s own message is still in its
`500` body, as it was before. `packages/adapters/hono/src/hono.test.ts`
already 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
- New:
`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` (with `check:test-typecheck`) is green, and `tsc --listFiles
-p tsconfig.test.json` counts 13 of 13 test files, the new one included.
- `@objectstack/hono`: 7 files, 154 tests passed; `typecheck` green.
- `@objectstack/plugin-hono-server`: 29 files, 336 tests passed;
`typecheck` green.
## Ablation (one leg per arm, `scripts/ablation-replace.mjs` in wrap
mode plus an outer `trap … EXIT INT TERM` restore)
In http-conformance's `vitest.config.ts`, the subject
`@objectstack/hono` is aliased to `packages/adapters/hono/src/index.ts`,
so a mutation of the source reaches the run without a build.
- **Catch-all arm** reverted to the old line. The anchor hit once, x1 to
x0; the blob went `3529a20128d4` to `51939c434b24`. Result: **8 failed,
5 passed**. All 8 catch-all declared pins went red. The controls and the
two auth-arm rows stayed green.
- **Auth arm** reverted to the old line. The anchor hit once; the blob
went `3529a20128d4` to `4314a9ba898d`. Result: **1 failed, 12 passed**.
Only the auth-arm declared pin went red, and its bare-`Error` control
stayed green.
- Restore was proven for each leg: the blob equals HEAD (`3529a20128d4`)
and `git diff HEAD` is empty.
## Gates
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 66 commands at `287fd99bda`. 65
exited 0. `pnpm check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT
MET: it reads the whole tree's `dist/`), which counts as NOT MEASURED
locally; CI runs it. `--ran` reports 66 derived, 65 run, 1 NOT-MEASURED,
0 UNRUN.
- Lint, narrowed: `eslint --no-inline-config` over the diff's three
`.ts` files exited 0, linting 3 files with 0 errors and 0 warnings.
Positive control: a stdin file in `packages/adapters/hono/src/`
importing a root namespace from `@objectstack/spec` exits 1
(`no-restricted-imports`). Repo-wide `pnpm lint` is CI's run.
## Changeset
`.changeset/23365-hono-declared-envelope.md`, `minor` for both packages.
`@objectstack/plugin-hono-server` is `minor` because `Clause-②: yes` (an
added export) takes at least `minor`. `@objectstack/hono` is `minor` to
match #16545's level: that change applied the same rule to the
direct-mount door and shipped as `minor` (`cefe068702`). The wording
follows that changeset's sections.
## Acceptance notes
- On this host, an undeclared 500 still carries the thrown message
verbatim, with no `looksLikeInternalErrorLeak` filter. It was already
the case before this change, and it is unchanged here. Noted only; not
filed.
- For a declared 5xx, the recogniser keeps the message unless
`looksLikeInternalErrorLeak` fires. 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.
- `main` gained `989aa483fd` and `0be162c75d` after the base. Neither
touches `packages/adapters/hono`, `plugin-hono-server`,
`http-conformance`, the dispatcher or `@objectstack/types`, and `git
merge-tree` is clean, so the branch was not merged; the merge queue
rebuilds on `main`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent b1cfa3d commit e1cefa5
4 files changed
Lines changed: 369 additions & 3 deletions
File tree
- .changeset
- packages
- adapters/hono/src
- plugins/plugin-hono-server/src
- qa/http-conformance/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| 47 | + | |
47 | 48 | | |
48 | 49 | | |
49 | 50 | | |
| |||
508 | 509 | | |
509 | 510 | | |
510 | 511 | | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
511 | 539 | | |
512 | 540 | | |
513 | 541 | | |
| |||
779 | 807 | | |
780 | 808 | | |
781 | 809 | | |
782 | | - | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
783 | 815 | | |
784 | 816 | | |
785 | 817 | | |
| |||
828 | 860 | | |
829 | 861 | | |
830 | 862 | | |
831 | | - | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
832 | 866 | | |
833 | 867 | | |
834 | 868 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
280 | 280 | | |
281 | 281 | | |
282 | 282 | | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
283 | 294 | | |
284 | | - | |
| 295 | + | |
285 | 296 | | |
286 | 297 | | |
287 | 298 | | |
| |||
0 commit comments