Files
cloudflare__vinext/tests/nextjs-compat
James Anderson 456ddbef11 fix(actions): run redirect targets through full request pipeline (#2785)
* fix(actions): run middleware for server action redirect targets

A server action that throws `redirect()` renders the target page inline
and returns its Flight payload with the action response, instead of
making the client re-request the target. Middleware had only run for the
action's own path, so the target's middleware never saw the request: an
action reachable on a public path could redirect to a middleware-gated
page and return that page's server-rendered payload, which the browser
client decodes and commits. Next.js has no such hole because the client
re-requests the target through the full pipeline.

Run the target's middleware against the synthetic GET before rendering
it, after the redirect render's headers context is installed so
`NextResponse.next({ request: { headers } })` overrides reach the page.
Middleware response headers merge into the action response.

Only a clean pass-through is rendered inline. A block, redirect, rewrite,
or status override diverts to the header-only 303 that already exists for
non-App-route targets, which the client re-requests through the full
request pipeline. Apps without middleware, and targets no matcher
matches, are unaffected.

* fix(actions): match redirect targets with request route identity

Follow-up to the target-middleware fix, addressing three ways the target
could still be evaluated as something other than the request the client
would have made.

`matchRoute` decodes pathname segments, so an encoded alias like
`/adm%69n` resolved to the `/admin` page while middleware and a real
navigation both saw `/adm%69n` — inline-rendering a route neither
reached. Match redirect targets with request route identity instead.

`cloneActionRedirectHeaders` carried `x-vinext-mw-ctx` onto the target's
request. In hybrid app+pages dev that header holds the Pages handler's
middleware result for the *action* path, which
`applyForwardedMiddlewareContext` replays in place of executing
middleware for the target. Strip it, which also keeps the internal header
out of the redirect render's `headers()`.

Middleware object matchers support `has`/`missing` header predicates, so
a matcher gated on `Accept` took a different branch against the render
request, which drops `Accept` with the action transport headers. Give
middleware a request that keeps it.

* docs(actions): note the redirect-target middleware divert trade-off

* fix(actions): run target middleware before importing redirect-target modules

A middleware-blocked redirect target still executed its route modules'
top-level code: the target was hydrated (dynamically imported) to decide
renderability before the middleware probe ran, so an unauthorized action
redirect could trigger module side effects, or turn a module-eval throw
into a 500 instead of the header-only fallback. Renderability is now
decided from the manifest's lazy thunks (__loadPage/__loadRouteHandler),
and hydration happens only after middleware passes the target through.

The probe leaked two more behaviors a real navigation would not produce:

- Pass-through middleware response headers merged verbatim onto the 303
  action wrapper, so a middleware-set Location gave the wrapper genuine
  HTTP-redirect semantics and fetch followed it before the action client
  could read x-action-redirect. Middleware header merges onto the wrapper
  now strip Location.

- The synthetic target requests were built with bare new Request(), which
  drops the Workers cf property, so target middleware keying off
  request.cf (geo checks) failed open. Both requests now re-attach cf via
  the metadata helper the request clone utilities already used.

* fix(actions): carry middleware cookie mutations onto the redirect target

When action-path middleware rotated or deleted an authentication cookie,
the synthetic redirect-target request was still built from the original
inbound Cookie header, so target middleware evaluated a credential the
response was simultaneously revoking and could render the protected page
inline. The middleware's Set-Cookie mutations now feed the same
request-cookie rebuild as the action's own cookies().set() calls, in
browser order (middleware first, action wins for the same name).

Also aligns two more target-request details with the real pipeline:

- The internal _rsc transport param is stripped from the redirect target
  before matching, middleware, and render, as app-rsc-handler does for
  navigations, so matchers cannot branch on a query no navigation
  carries. The client-facing x-action-redirect header keeps the verbatim
  URL; a diverted re-request still goes through real validation.

- The middleware header merge onto the action wrapper now restores all
  protocol headers (Content-Type, x-action-redirect and friends) rather
  than only stripping Location, so a pass-through middleware can neither
  replace the wrapper's destination nor flip the client into treating
  the Flight body as non-RSC.

* fix(actions): scope redirect-target cookies and framing to browser behavior

Applying every pending Set-Cookie to the redirect target's Cookie header
ignored the cookie's Path attribute, so a mutation scoped to another
path (admin=1; Path=/account) reached a target like /admin that a real
browser navigation would never send it to, and target middleware could
authorize on it. Mutations now apply only when their Path, or the RFC
6265 default path derived from the action URL, path-matches the target.
Cookies from cookies().set() and draftMode() always carry Path=/ and are
unaffected.

Also adds Content-Length to the wrapper's protected headers: a
pass-through middleware value would misframe the freshly generated
Flight stream and let adapters truncate or reject the action response.

Drops the fixture node_modules symlink an e2e run left staged; on
Windows checkouts with core.symlinks=false it materializes as a plain
file that defeats the Playwright server's link-creation guard. The
gitignore rule loses its trailing slash so it covers symlinks and stops
these from getting staged again.

* fix(actions): use redirect target CSP nonce

* docs(actions): clarify redirect cookie projection

* fix(actions): forward middleware headers to redirect targets

* fix(actions): preserve redirect response framing

* fix(actions): preserve middleware request overrides

* fix(actions): run redirect targets through full request pipeline

* fix(actions): match redirect target header semantics

* fix(actions): mirror Next cookie forwarding

* fix(actions): preserve forwarded cookie ordering

* fix(actions): avoid stale forwarded cookies

---------

Co-authored-by: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
2026-08-03 00:28:04 +01:00
..
2026-04-09 09:14:13 +01:00