Skip to content

fix(vercel): run edge middleware when isr is enabled - #17687

Open
asmyshlyaev177 wants to merge 4 commits into
withastro:mainfrom
asmyshlyaev177:isr_vercel_middleware_cache
Open

fix(vercel): run edge middleware when isr is enabled#17687
asmyshlyaev177 wants to merge 4 commits into
withastro:mainfrom
asmyshlyaev177:isr_vercel_middleware_cache

Conversation

@asmyshlyaev177

Copy link
Copy Markdown

Changes

With isr enabled, middlewareMode: 'edge' is silently inert.

The adapter builds and deploys _middleware.func, but every on-demand route's
dest is set to the ISR function, so nothing ever routes to it. Middleware
still runs — but only inside the ISR function, which Vercel skips entirely on a
cache hit. The observable result is middleware that works on a cold entry and
then stops running once the entry is warm, with no error and no log.

This is the ordering problem: middleware has to run before the cache is
consulted, not behind it.

  • Route on-demand pages at _middleware rather than _isr when a middleware
    entry point exists, so the edge function is actually reached.
  • Collect those route patterns and inline them into the generated middleware,
    so next() forwards to /_isr?x_astro_path=… for a route the ISR function
    backs, and /_render for one it doesn't. Cached responses are still served
    from cache; only the entry point moves.
  • _image and _server-islands keep going straight to _render, unchanged.
  • Routes matched by isr.exclude still resolve to _render through next().
  • Prerendered pages are untouched: no route entry, served as static files.

x_astro_path carries the original pathname, so the ISR cache key stays the
request path rather than collapsing to /_isr; x_astro_path_token is the
build token added in #17370. Without isr, or without a middleware file,
nothing about the output changes.

Testing

New: packages/integrations/vercel/test/isr-edge-middleware.test.ts — 16 tests
over two fixtures.

isr-with-edge-middleware asserts against the real build output, since the bug
is entirely a property of the emitted config.json:

  • pages, dynamic routes, endpoints and the 404 resolve to _middleware
  • _image and _server-islands still resolve to _render
  • prerendered pages get no route entry and ship as static HTML
  • configured redirects still resolve ahead of the middleware
  • the ISR prerender config and its expiration survive

and then imports the generated middleware.mjs with fetch stubbed, to check
where next() actually forwards:

  • a cached route → /_isr, with x_astro_path and a token
  • a dynamic route → /_isr?x_astro_path=/cached/42, the real path, because that
    path is the cache key
  • an isr.exclude route → /_render
  • a query string does not leak into x_astro_path
  • the response still reaches the middleware, headers intact

isr-edge-no-middleware covers middlewareMode: 'edge' with no middleware file
present: routes go straight to _isr as before and no middleware function is
built.

Verified as a regression guard by reverting src/ and re-running: 4 of the 16
fail, all of them on dest.

Also ran the full suite — core unit (3307), core integration (1240), all 18
integration packages including @astrojs/vercel (58), language-tools (98), and
e2e in chrome and firefox. No new failures; the pre-existing ones
(test/fonts.test.ts cancellations, a handful of e2e) don't touch the adapter.

One caveat worth stating plainly: this is verified against build output and the
generated module, not against a live Vercel deployment. Confirmation on a real
project would be welcome.

With `isr` set, every on-demand route's `dest` pointed at the ISR
function. The middleware edge function was still built and deployed,
but nothing routed to it, so `middlewareMode: 'edge'` was silently
inert: middleware only ran inside the ISR function, and ISR skips that
function entirely on a cache hit.

Point those routes at the middleware function instead, and have the
generated `next()` forward to `_isr` for routes the ISR function backs,
so cached responses are still served. Routes matched by `isr.exclude`
keep forwarding to `_render`, and `_image` / `_server-islands` stay on
the serverless function as before.
@changeset-bot

changeset-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d9b0c39

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

This PR includes changesets to release 23 packages
Name Type
@astrojs/vercel Patch
@test/astro-vercel-basic Patch
@test/astro-vercel-image Patch
@test/astro-vercel-integration-assets Patch
@test/vercel-isr-edge-no-middleware Patch
@test/vercel-isr-with-edge-middleware Patch
@test/vercel-isr Patch
@test/vercel-max-duration Patch
@test/vercel-edge-middleware-with-edge-file Patch
@test/vercel-edge-middleware-without-edge-file Patch
@test/astro-vercel-no-output Patch
@test/astro-vercel-prerendered-error-pages Patch
@test/astro-vercel-redirects-serverless Patch
@test/astro-vercel-redirects Patch
@test/vercel-server-islands Patch
@test/astro-vercel-serverless-prerender Patch
@test/astro-vercel-serverless-with-dynamic-routes Patch
@test/astro-vercel-static-assets Patch
@test/vercel-static-headers Patch
@test/astro-vercel-static Patch
@test/vercel-streaming Patch
@test/astro-vercel-with-web-analytics-enabled-output-as-static Patch
vercel-hosted-astro-project Patch

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

@github-actions github-actions Bot added the pkg: integration Related to any renderer integration (scope) label Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: integration Related to any renderer integration (scope)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant