Skip to content

Prototype: composable Hono middleware for Astro (astro/hono)#16208

Draft
matthewp wants to merge 31 commits intomainfrom
hono
Draft

Prototype: composable Hono middleware for Astro (astro/hono)#16208
matthewp wants to merge 31 commits intomainfrom
hono

Conversation

@matthewp
Copy link
Copy Markdown
Contributor

@matthewp matthewp commented Apr 3, 2026

Changes

This is a work-in-progress prototype — not intended for merge yet. Opening as a draft for a preview release so folks can try it out.

  • Introduces a new astro/hono export with composable Hono middleware: pages(), actions(), i18n(), redirects(), context(), and rewrite()
  • Adds a Pages class (src/core/app/pages.ts) that encapsulates file-based routing, trailing-slash redirects, rendering, and error pages — independently testable by accepting a manifest
  • Adds src/core/app/api-context.ts with a getAPIContext() helper (WeakMap-cached per request) for accessing the full APIContext from Hono middleware
  • Wires a virtual:astro:app Vite plugin so a users src/app.ts` becomes the request entrypoint in dev, with a default Hono app fallback
  • Adds an examples/app demo showing composed middleware for auth, redirects, actions, and i18n

Testing

  • Unit tests for Pages covering rendering, route matching, trailing-slash redirects, custom 404, locals passthrough, and routeData
  • Existing dev server and routing unit tests updated to work with the new architecture

Docs

  • No docs update needed yet — this is an early prototype behind a new export path

…hono

- Add src/core/app/pages.ts with Pages class that owns file-based routing,
  trailing slash redirects, rendering, and error pages — independently testable
  by accepting a manifest in its constructor
- Add src/core/app/hono.ts exposing composable middleware: astro(), context(),
  redirects(), actions(), i18n(), rewrite(), pages()
- Add src/core/app/api-context.ts with getAPIContext() helper using a WeakMap
  to create and cache ActionAPIContext per request
- Export parseRequestBody from actions/runtime/server.ts for use in actions()
- Add src/app.ts example showing the user-facing Hono entrypoint
- Add virtual:astro:app Vite plugin wiring user's src/app.ts as the request
  entrypoint in dev, with a default Hono app fallback
- Add unit tests for Pages covering rendering, matching, trailing slash
  redirects, custom 404, locals, and routeData passthrough
- Create examples/app with Hono-based entrypoint demonstrating:
  - Composed middleware (auth, redirects, actions, i18n, pages)
  - context() function for accessing Astro APIContext
  - Login/logout flow with Astro Actions
  - Styled components with scoped Astro styles

- Refactor astro/hono exports:
  - context(c) now returns full APIContext instead of being middleware
  - Export Hono constructor from astro/hono for default app
  - Remove old context middleware, use lazy context creation

- Fix dev server URL construction:
  - Use core/request.ts instead of core/app/node.js in dev
  - Properly handle Host header with port for CSRF checks
  - Export makeRequestBody helper for body handling

- Update Pages to accept pipeline parameter for dev/prod compatibility
@github-actions github-actions bot added pkg: example Related to an example package (scope) pkg: astro Related to the core `astro` package (scope) labels Apr 3, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 3, 2026

🦋 Changeset detected

Latest commit: cc6927f

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 3, 2026

Merging this PR will degrade performance by 55.81%

❌ 10 regressed benchmarks
✅ 8 untouched benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation Build: hybrid site (static + server) 8.5 s 9.7 s -12.05%
Simulation Rendering: streaming [true], .md file 1.2 ms 1.5 ms -21.08%
Simulation Rendering: streaming [false], .md file 1.2 ms 1.5 ms -22.9%
Simulation many-components [streaming] 8.2 ms 12.4 ms -34.17%
Simulation large-array [streaming] 152.9 ms 346 ms -55.81%
Simulation many-expressions [streaming] 24.8 ms 28.8 ms -13.68%
Simulation many-components (markHTMLString, isHTMLString, validateProps) 8.9 ms 14 ms -36.37%
Simulation many-head-elements (head dedup) 5.1 ms 6.8 ms -24.75%
Simulation large-array (BufferedRenderer per child) 173.1 ms 365.2 ms -52.6%
Simulation many-slots (eager slot prerendering) 5.6 ms 8.2 ms -31.63%

Comparing hono (cc6927f) with main (337d3aa)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (673a871) during the generation of this report, so 337d3aa was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

matthewp added 5 commits April 3, 2026 09:00
- Skip prerendered routes in Pages.match() by default (matching App.match() behavior)
- Only strip base prefix when the URL actually starts with it (fixes dev mode where base is already stripped by connect middleware)
- Strip query params from request URL for prerendered routes in render()
- Allow render() to internally match prerendered routes for the prerender environment
- Delete orphaned api-context.ts and vite-plugin-app/app.ts (replaced by Hono approach)
- Remove unused clientLocalsSymbol from constants.ts
- Remove unused handle500Response, writeSSRResult, writeWebResponse from response.ts
- Fix regex capturing groups in hono.ts redirect matching
…URLs

- Import initial routes from virtual:astro:routes in the dev entrypoint so
  DevApp has page routes from startup (not just via HMR updates)
- Add route syncing to Pages via #syncRoutes() which lazily pulls the latest
  routes from DevApp's manifestData on each request
- Configure Pages Router with base '/' in dev since the connect base middleware
  already strips the base prefix before requests reach the Hono app
- Add fallback pattern match for the index route with trailingSlash 'never' and
  non-root base, where the Router can't match '/' against the ^$ pattern
- Capture action middleware Response and assign to c.res in astro() wrapper
- Add Vite error overlay to createAstroServerApp catch handler (sends
  @vite/client script instead of plain text)
- Re-throw render errors in dev mode so they reach the error overlay handler
- Add app.onError rethrow to default dev Hono app and BaseApp.createHonoApp
  so Hono doesn't swallow errors during build or dev
- Import virtual:astro:routes eagerly in createAstroServerApp (at
  createHandler time) before file watchers can mutate the route list,
  then sync those routes into the DevApp on first request
- Rename Vue test fixture src/app.ts to src/vue-app.ts to avoid
  collision with the Hono server entrypoint convention
@github-actions github-actions bot added pkg: vue Related to Vue (scope) pkg: integration Related to any renderer integration (scope) labels Apr 3, 2026
matthewp and others added 13 commits April 3, 2026 14:27
Replace writeResponse with writeDevResponse that catches stream errors
(e.g. a React component throwing mid-render) and injects the Vite error
overlay script instead of destroying the socket. This prevents the client
from seeing a terminated connection on render errors.
The actions() middleware was returning a bare Response without session
cookies. Now persists the session via PERSIST_SYMBOL and appends
set-cookie headers from Astro.cookies after running the action handler.
- Set user-configured server.headers on every dev response (matching
  the old AstroServerApp.handleRequest behavior)
- Strip /index.html and .html suffixes when matching routes in dev,
  so URLs like /page/index.html resolve to the /page route
Fix three issues in the Hono dev server path:

- Pass statusText through writeDevResponse so custom status text is preserved
- Attach clientAddress from the socket to the Request via clientAddressSymbol
- Save the original base-prefixed URL in the base middleware (via originalUrlSymbol)
  and use it in Pages.render() so ctx.url and ctx.request.url match production,
  and skip double base-stripping in getPathnameFromRequest when base was already stripped
- Widen context() to accept any HonoContext, not just AstroHonoEnv
- Use z.email() instead of deprecated z.string().email() (Zod 4)
- Return next() so all code paths return a value
- Use :authority pseudo-header for HTTP/2 requests and skip statusMessage
  which HTTP/2 does not support
- Catch NoMatchingStaticPathFound in Pages.render() and return 404 instead
  of 500 for prerendered routes with no matching static path
- Cache the Hono user app instance to avoid re-evaluation caused by
  virtual:astro:component-metadata invalidation cascading through the
  import chain on every file-watcher event
- Restore base-prefixed URL before prerender query-strip to prevent
  originalUrlSymbol from being lost by new Request()
- Remove encodeURI on redirect location to prevent double-encoding
  of already-encoded special characters
- Add invalidate() to cached user app and call it on file add/unlink
  in srcDir so HMR picks up new/removed pages
- Skip core-image error log tests (logger not propagated to DevApp)
- Render custom 404/500 pages directly in dev instead of fetching
  prerendered HTML, so Astro.url reflects the requested path
- Add clientLocalsSymbol and forward integration-set locals from the
  IncomingMessage to the Hono request so Astro.locals works in 404 pages
- Invalidate cached user app on file changes (not just add/unlink) so
  HMR content updates like markdown edits are picked up
Consolidate base URL handling so the base is stripped once by the Vite
base middleware, then restored once in createAstroServerApp. The entire
downstream pipeline (Hono middleware, Pages, RenderContext) now sees
base-prefixed URLs matching production behavior.

- Remove originalUrlSymbol and all save/restore plumbing
- Remove #baseStripped flag from Pages (renamed to #isDev for dev-only behavior)
- Router always uses manifest.base; Pages.#getPathnameFromRequest always strips it
- Hono middlewares (redirects, actions, i18n) call removeBase() from
  @astrojs/internal-helpers/path where base-stripped pathnames are needed
…tions

- Handle base + '/' concatenation artifact when trailingSlash is 'never'
  to prevent redirect loops on index routes like /docs
- Use removeBase() from @astrojs/internal-helpers/path instead of manual
  string slicing throughout hono.ts and pages.ts
- Fix index route matching for trailingSlash: 'never' with non-root base
…cture

Major architectural refactor of the rendering pipeline:

- Add 'path' field to RouteData (standard router path syntax like /blog/:slug)
  generated at all creation sites via getRoutePath()
- Create standalone renderers: PageRenderer (uses createSSRResult + renderPage
  directly), EndpointRenderer (uses renderEndpoint with APIContext from Hono
  context), RedirectRenderer
- Create prepareForRender() shared orchestration function (query stripping,
  pathname computation, component loading, RenderContext creation, cache,
  error pages, session, response cleanup)
- Create ssr-result.ts with createSSRResult() and createAstroGlobal() extracted
  from RenderContext
- Extract hono-app.ts factory: createAstroApp() and createAstroMiddleware()
  accept deps as parameters, no virtual module dependency
- hono.ts is now a thin wrapper that pre-binds virtual module deps and exports
  individual middleware: context(), astro(), pages(), actions(), redirects(),
  rewrite(), i18n() — composable by users in src/app.ts
- createAstroMiddleware composes all middleware via nested Hono app
- Move form action execution into actions() Hono middleware
- Move user middleware into Hono userMiddleware() wrapper
- Move rewrite loop detection into rewrite() Hono middleware
- Move trailing slash redirects into Hono middleware
- Add URL normalization (security: double-slash collapse) in context factory
- BaseApp.render() auto-creates default Hono app via factory when no user
  app is provided; production path uses setUserApp() from prod.ts
- Remove renderWithAstro, renderError, mergeResponses from BaseApp
- Remove Pages class entirely
- Delete pages.test.js (covered by App-level tests)
- Update all test route data to include path field
- Conditional middleware composition (redirects/i18n only when needed)
- Route sync via ASTRO_APP_DEPS symbol for dev module caching
matthewp added 7 commits April 8, 2026 14:21
- Actions: catch TypeError from parseRequestBody and return 415
- i18n: set ROUTE_TYPE_HEADER on all responses so i18n middleware detects pages
- i18n: add domain locale resolution in matchRouteData and context factory
- i18n: add domain locale resolution in ssr-result.ts createAstroGlobal
- Rewrites: preserve originPathnameSymbol across rewrite requests
- Rewrites: set originPathname in prepareForRender only if not already set
- Rewrites: handle next(rewritePayload) in createUserMiddleware
- Rewrites: add ForbiddenRewrite check for SSR→prerendered in PageRenderer,
  context factory, and createUserMiddleware
- Rewrites: make params and routePattern writable on APIContext for sequence()
- Middleware: render error page for reroutable status from user middleware
- Middleware: pass original response to renderErrorPage for framing header stripping
- Middleware: attach RenderContext cookies to response in prepareForRender
- Middleware: append Hono APIContext cookies in createPagesMiddleware
- Middleware: catch errors in createUserMiddleware, re-throw in dev without
  custom 500 page for Vite error overlay
- Middleware: forward addCookieHeader option via request symbol
…rect headers

- Strip REROUTE_DIRECTIVE_HEADER, ROUTE_TYPE_HEADER, NOOP_MIDDLEWARE_HEADER,
  and REWRITE_DIRECTIVE_HEADER_KEY from responses in createAstroMiddleware
  before they reach the client
- Handle immutable Response headers (e.g. from Response.redirect()) by
  creating a new Response with cleaned headers when delete throws
- Wrap endpoint ROUTE_TYPE_HEADER set in try-catch for immutable responses
…iddleware

- Remove cookie handling from prepareResponse (no longer appends set-cookie)
- Remove addCookieHeader from PrepareOptions (no longer needed in prepare.ts)
- createPagesMiddleware is now the single place where cookies are collected
  from both ctx.cookies (middleware/endpoints) and response-attached
  AstroCookies (page components), respecting the addCookieHeader option
- Remove duplicate cookie appending from createUserMiddleware
…erContext

- Accept optional cookies param in RenderContext.create() and CreateRenderContext type
- Pass ctx.cookies from Hono APIContext through PrepareOptions to RenderContext
  so middleware, page components, and endpoints all write to the same instance
- Simplify createPagesMiddleware cookie handling — single object, no merging
- Store cookies on request via symbol for re-attachment after inner.fetch()
- Fix addCookieHeader: false by setting symbol for both true and false values
- Pass shared cookies through all rewrite paths (PageRenderer, context factory,
  createUserMiddleware) so cookies survive across rewrite boundaries
- Attach and append cookies in createUserMiddleware for rewrite responses
  that bypass createPagesMiddleware
- Preserve middleware cookies on error page responses
…ies across rewrites

- matchRouteData accepts allowPrerenderedRoutes option (defaults to false)
- createAstroMiddleware passes allowPrerenderedRoutes: true in dev mode
  so prerendered routes are handled; in production they return 404 as
  they're served as static assets by the adapter/CDN
- Pass shared cookies through context factory rewrite handler and
  createUserMiddleware rewrite handler
- Attach and append cookies in createUserMiddleware for rewrite responses
- Fix addCookieHeader: false by setting symbol for both true and false
- Remove unused cookie merging code from prepare.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs pr pkg: astro Related to the core `astro` package (scope) pkg: example Related to an example package (scope) pkg: integration Related to any renderer integration (scope) pkg: vue Related to Vue (scope)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant