Repository navigation
fix(runtime): run page middleware around the final HTTP response - #624
Merged
Brooooooklyn merged 2 commits intoNov 2, 2026
Merged
Brooooooklyn merged 2 commits into
Brooooooklyn merged 2 commits into
Conversation
Fixes voidzero-dev#564 Run per-route `defineHandler` middleware around the final HTTP Response instead of around a temporary leaf body. Pages finish load/render (and action revalidation) inside an endpoint step; routes still go through `convertReturnValue`. Nested compositions flatten; middleware overloads keep leaf `withValidator` phantoms (`__validators` / `__output`). Most of this patch was written by a coding agent. The change is large on purpose: clearing `finalized` while keeping two exit paths was not a single owner for "handler result → Response". Happy to change if maintainers prefer one of the alternatives below. - Add `executeComposedHandler` and composition metadata on composed handlers. - Page protocol: composed loaders/actions render inside the middleware endpoint; validation `issues` 400s become page field errors there. - `createHttpRouteHandler`: composed handlers return the middleware Response; plain handlers still use `convertReturnValue`. - Tests for page middleware cases, route `etag` on the final body, and validator types through middleware. - `packages/void` unit: `define-handler.test.ts`, `pages-protocol.test.ts` - `packages/void` typecheck (incl. middleware + `withValidator`) This PR is the shape we landed on: one executor (`executeComposedHandler`) that runs middleware around the final HTTP Response, with pages doing load/render (and action revalidation) inside the endpoint step. We also looked at a smaller patch that clears `finalized` and keeps separate exits for routes vs pages. That still left two places that turn a handler result into a Response, so we did not ship it. Another option is to wrap middleware only around the leaf mutation on POST and re-render the page outside. After-next would then only see JSON/204, not the HTML the client actually gets. A third option is to keep returning the raw leaf value from `defineHandler` and let every caller adapt it. That matches less with the route path (`convertReturnValue` / final Response) and is easy to misuse from page code. On how the page Response enters the context: this PR sets `c.res` from a full `Response` so after-next can mutate it. Rebuilding with `c.newResponse()` / `c.body()` would merge headers set before `next()` more like stock Hono, but needs care with deferred / streaming. For types: this PR uses explicit `TypedHandler<V, R>` overloads per middleware arity. A single `H extends TypedHandler` overload is shorter and may be fine if maintainers prefer that. Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…sions Keep the approach (per-route middleware wraps the final page response) but make the change smaller and fix what review found: - executeComposedHandler returns the final c.res, as Hono's compose does. It returned a middleware's short-circuit Response instead, so outer middleware edits after next() were lost, and a composed action re-rendering a composed loader failed with "body disturbed". - Move the composition helpers to an internal module so they are not exported from void/handler. - One rest-tuple overload keeps a typed leaf's validators and output, instead of one overload per middleware count. - Pages: runLoader/runPageHandler run the leaf and render inside the chain; handlePageGet keeps its body. Drop the unused routing.ts change (Responses already pass through convertReturnValue) and the dead runtime metadata. - A ValidationError thrown by action middleware before the response exists becomes field errors again; one from the loader re-render is not caught, so the loader no longer runs twice. - Read shared and headDefaults at render time, so per-route middleware can set them on GET and after an action. - Tests go through a real Hono app (the direct-call tests passed on main): loader/action/field errors (JSON and HTML), headers and shared, short-circuit with outer edits, ValidationError handling, deferred props, etag. Type test uses real middleware and checks the whole handler type, including nesting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brooooooklyn
force-pushed
the
cursor/definehandler-design-a-executor-3fc7
branch
from
November 2, 2026 05:07
27a131a to
763ce4b
Compare
Brooooooklyn
approved these changes
Nov 2, 2026
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.
Fixes #564
Problem
defineHandler(middleware..., handler)stored the handler's result inc.res, which finalizes the Hono context. A page builds its own response after the loader or action runs, and Hono dropped that response:GETpageBody has already been usederrorsThe middleware overloads also dropped a
withValidatorleaf's validator types.Fix
defineHandlerrecords its middleware and leaf (internal moduleruntime/handler-composition.ts, not exported). Nested compositions are flattened.executeComposedHandlerworks like Hono's compose: a middleware Response replacesc.res, the endpoint's Response becomesc.res, middleware can editc.resafternext(), and it returns the finalc.res.runLoader/runPageHandler): run the leaf and build the page response inside the chain. Redirects and other Responses from the loader or middleware are returned as before.sharedandheadDefaultsare read at render time, so per-route middleware can set them.ValidationErrorfrom the leaf, or from middleware before the response exists, becomes field errors. One from the loader re-render is not caught.defineHandler(...middleware, typedHandler)returns the typed handler unchanged, sowithValidatorvalidators and output reachvoid/routes.Tests
packages/void/test/unit/routing-runtime.test.tsruns pages through a real Hono app (every case fails on main or on the first version of this PR):next()andsharedreach the page (GET and action)next()ValidationErrorfrom action middleware → field errors; from the loader re-render → loader runs once; after the response exists → erroretag()tags each page formAlso
define-handler.test.ts(route middleware header edits) androute-type-extraction.typecheck.ts(validators and output kept through middleware, including nesting).Verification
vp test run packages/void/test/unit: 362 files, 8246 tests passedvp run test --base origin/main: all affected tests passedvp run --filter ./packages/void typecheck,vp lint,vp fmt: clean🤖 Generated with Claude Code