Repository navigation
Conversation
Hono's compose only hands `err instanceof Error` to `onError`, and
hono-base rethrows anything else out of `fetch`. A `throw 'text'` or
`throw { code: 1 }` from a route handler, a middleware or a validator
therefore skipped the application's error handler (the `@Catch()`
catch-all and `ConfigService.onError`), the adapter's default 500 and its
log line; Bun reset the connection instead.
Every handler the adapter registers now converts a non-Error throw into
an internal carrier Error at its own boundary, so Hono treats it exactly
like an Error thrown at the same place (same dispatch level, same
middleware unwinding). `onError` unwraps the carrier before the
application's handler sees it, so the handler receives the original
value unchanged - the contract ergenecore already keeps. With no handler,
or one that declines, the adapter answers its default 500 JSON envelope
and logs the value JSON-serialised (falling back to Object#toString for
a value JSON cannot serialise), without a stack.
The guard on the route handler costs one extra promise per request;
measured over a socket with wrk against a bare Hono app the difference
stayed within run-to-run noise (~0.3% mean over three rounds).
Core 0.13 adds the exception handler (@ExceptionHandler / @catch), which reaches the adapter through the same onError contract 0.12 uses, so the adapter needs no code change to run under it. The range is widened, not moved: 0.12 applications keep resolving this release. devDependencies stay on 0.12 because 0.13.0 is not on npm yet.
One patch changeset for the non-Error throw fix and the widened core peer range, so both ship in the same hono-adapter patch release.
…hes its bus Core sees every step of a request except the ones that frame it: when it started and ended, its final status, and who answered when nothing in core's chain did. Only the adapter sees those, so core 0.13 hands its devtools bus to adapter.attachDevtools(bus) in a process asena console spawned, before any route is registered. The bus is declared as a local structural type (HonoDevtoolsBus): the adapter builds against published core, which has no devtools, and must keep working with a core that never calls the method. When attached: - start() serves a fetch that opens a record per request and runs the dispatch inside bus.run(), so steps are correlated by async context. That is what makes an override: true middleware, which receives hono's raw Context rather than the Asena wrapper, land in the right record. The record closes with the response status, or as an adapter 500 if fetch rejects. Websocket upgrades are not recorded. - the adapter's own 404 (no onNotFound, or one that declined) steps not-found, named after the adapter; - defaultErrorResponse, reached with no onError, a declining one or a throwing one, steps adapter-default; - a legacy validator: schema, which core never sees run, steps validator named after its target (the validator class is not on the route the adapter receives), on the cached and the per-request path, before the check, so a failure keeps it. A middleware answering false needs no adapter step: core recorded it when it was called, and the envelope closes the record with the 403. When attachDevtools is never called, start() serves app.fetch itself and the validator middlewares are the ones registered before; a test pins the fetch identity.
attachDevtools() is new public API and the adapter gains behaviour under asena console, so this is a minor, not a patch.
The console's request inspector showed `answeredBy: adapter-default` for a 400 from a legacy `validator:` route, and named the validator step after its target only (`json`). The adapter merely renders the ValidationError's own response there, so the answer belongs to the validator that failed. Each validator step now remembers the ValidationError it threw; when that error reaches the adapter's default error response, the record is closed with that step as `answeredBy` and no adapter-default step is appended. A @catch or onError that answers still shows as the answering step after the validator. Controller throws, including a ValidationError thrown by hand, stay adapter-default. The step is named `<ValidatorClass>.<target>` when the route carries a `validatorName`. Core passes a plain `{ [target]: { handle } }` map with bound handles, so the class name can only come from core; the field is read structurally because the published core's RouteParams does not declare it. Without it the step keeps the target as its name.
The request-inspector changeset now says a validation failure nobody answered is credited to the validator, which is what users of the console will see change.
The legacy validator step was named from a route-level `validatorName`
that core never passes, so in a real app the step stayed `json`. Core
hands the adapter `{ handle, override }` per target with a bound handle,
so the class is not recoverable from the handle itself.
Ergenecore already reads an optional `name` on each ValidatorHandler.
Read the same field here so both adapters follow one contract: core sets
`name` to the validator's component name in PrepareValidatorService, and
the step becomes `<component>.<target>`. Without it the step keeps the
target name, as before. The route-level field is no longer read.
The previous fix moved the step name to an optional `name` on each ValidatorHandler, while ergenecore, fixing the same finding at the same time, moved the other way to the route params' `validatorName`. The two adapters ended up reading different carriers, and core sets neither, so a real run still named the step `json`. A bound method does not reveal its class, so the name has to come from core. Read the route's `validatorName`, structurally because the published core's RouteParams does not declare it - the field ergenecore reads at fc271c5 - so core needs one change for both adapters: pass the validator's component name as `validatorName` in its registerRoute call. The tests now build each handle as core's PrepareValidatorService does (a bound method, no name) and put the name on the route; a test pins that a `name` on the handle is not read.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three changes for the core 0.13 release. Values thrown that are not
Errorinstances now reach the application's error handler (@Catch(),ConfigService.onError) instead of resetting the connection. The adapter implements core's optionalattachDevtools(bus)hook, so every HTTP request shows up in theasena consolerequest inspector. And the core peer range accepts 0.13.When the console is not attached,
start()handsapp.fetchitself toBun.serve; nothing is recorded and no envelope is added.Changes
72fa1e7): Hono only passesErrorinstances toonErrorand rethrows anything else out offetch, sothrow 'text'orthrow { code: 1 }skipped@Catch(),onErrorand the default 500, and Bun reset the connection. Every handler the adapter registers (route handler, Asena andoverridemiddlewares, both validator paths, the contract validator, the static endpoint) now wraps such a value in an internal carrierError;onErrorunwraps it, so the application's handler receives the original value by identity, as on ergenecore. With no handler, or one that declines, the adapter answers its default 500 JSON body and logs the value.8c13111):@asenajs/asenapeer^0.12.0->^0.12.0 || ^0.13.0. The devDependency stays on the published^0.12.0.attachDevtools(bus)(16cb897,d3de7d2): when attached,start()serves afetchthat opens one record per request and runs dispatch inside the bus, so core's steps (middleware, controller,@Catch,onError,@NotFound) land in it, includingoverride: truemiddlewares. Adapter-only steps:adapter-default(namedHonoAdapter) for the built-in 404 and the default error response, and avalidatorstep around each legacyvalidator:middleware. The bus type is a local structural interface (HonoDevtoolsBus). WebSocket upgrades are not recorded.bfe9bcb,014f201): a legacy validation failure that no@Catch/onErroranswered is credited to the failing validator step, notadapter-default. The step is named<ValidatorClass>.<target>from the route'svalidatorName(the same field ergenecore reads, passed by core 0.13); without it the step is the bare target.Release
@asenajs/hono-adapter5.2.0 (minordevtools-envelope, patchnon-error-throw, which also carries the peer widening).@asenajs/asena^0.12.0 || ^0.13.0,hono>=4.12.9.@asenajs/asenadevDependency to^0.13.0and runbun update.Testing
test/NonErrorThrow.test.ts(43 tests): string and plain-object throws from every registration site x handler answers / declines / absent, plus a circular value. All 36 cases of the first matrix failed withECONNRESETbefore the fix.test/Devtools.test.ts(34 tests) against a fake AsyncLocalStorage-backed bus: not-attached identity, 404 and error defaults, concurrent requests,overridemiddlewares, and the validator attribution matrix.wrkover a socket stayed within run-to-run noise.Known follow-ups
Context.definecontract failure is still recorded asadapter-default(ergenecore credits the contract).next()and then returnsfalsegets a 403 whoseansweredBynames the downstream step.Responseis returned, so the duration excludes the body.