Skip to content

fix(runtime): run page middleware around the final HTTP response - #624

Open
0froq wants to merge 1 commit into
voidzero-dev:mainfrom
0froq:cursor/definehandler-design-a-executor-3fc7
Open

0froq wants to merge 1 commit into
voidzero-dev:mainfrom
0froq:cursor/definehandler-design-a-executor-3fc7

Conversation

@0froq

@0froq 0froq commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #564

Summary

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.

Changes

  • 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.

Testing

  • packages/void unit: define-handler.test.ts, pages-protocol.test.ts
  • packages/void typecheck (incl. middleware + withValidator)

Alternatives

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.

🐸

Fixes voidzero-dev#564

## Summary

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.

## Changes

- 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.

## Testing

- `packages/void` unit: `define-handler.test.ts`, `pages-protocol.test.ts`
- `packages/void` typecheck (incl. middleware + `withValidator`)

## Alternatives

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>
@cpojer
cpojer requested a review from Brooooooklyn October 28, 2026 00:18

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

defineHandler(middleware, handler) breaks page loaders and actions: 500 on GET, empty 204 after an action, validator types dropped

1 participant