Skip to content

fix(app-router): preserve forwarded action redirect wrappers#1582

Open
NathanDrake2406 wants to merge 4 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/app-action-node-middleware
Open

fix(app-router): preserve forwarded action redirect wrappers#1582
NathanDrake2406 wants to merge 4 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/app-action-node-middleware

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

@NathanDrake2406 NathanDrake2406 commented May 23, 2026

Fixes #1481

Overview

Field Details
Goal Match Next.js server-action redirect behavior for stale visible-route and forwarded action POSTs.
Core change Render same-origin action redirect targets into the POST Flight response and keep wrapper status semantics where Next.js forwards the action.
Boundary App Router server-action execution, browser action dispatch, and route segment runtime metadata.
Primary files app-server-action-execution.ts, app-browser-entry.ts, app-segment-config.ts, server-actions.spec.ts
Expected impact Fixes action redirects that previously surfaced as HTTP 303 responses where the client expects a 200 Flight wrapper.

Why

For App Router actions, the POST URL is the visible route, while next-action identifies the server reference. If the visible route is stale or belongs to a different action worker, Next.js forwards the action and still returns a client-consumable Flight wrapper for redirecting actions. Vinext has one deployed worker, but the client observable contract is the same: the action response must carry redirect headers plus a valid Flight body when the action redirect can be rendered internally.

Area Principle / invariant What this PR changes
Server action redirects Same-origin redirects should produce a usable Flight response for client actions. Renders the redirect target during the action POST and returns the redirect control headers with the Flight body.
Forwarded and stale action shape Forwarded action redirects use wrapper status semantics instead of plain HTTP redirect semantics. Uses wrapper status for explicit forwarded actions, stale child-route redirects, and cross-runtime route redirects.
Browser action dispatch Action POSTs target the visible route and identify the action through headers. Sends the public next-action header and keeps action POST URLs on the visible route.

What Changed

Scenario Before After
Same-origin action redirect Browser often hard-navigated from an empty redirect response. Server returns redirect headers with target Flight payload in one POST response.
Redirect to App route handler A matched route.ts target could be treated as a page render target. Non-page App route targets use header-only redirect fallback.
Stale child route action redirects Upstream candidate failed with 303 where Next.js expects 200. Stale child and sibling route redirects return the wrapper response status.
Cross-runtime action redirect shape Runtime ownership was not represented in route config. Segment runtime is parsed and threaded into action redirect status selection.
Forwarded non-redirect action side effects Skipping the page rerender could drop cookies and revalidation headers. The no-rerender response still carries Set-Cookie and x-action-revalidated.
Redirect Flight compatibility Redirect action Flight bodies bypassed deployment-skew hard navigation. Incompatible redirect Flight responses hard-navigate to the redirect target.
Invalid action response content Non-Flight failures could be classified as RSC compatibility failures. Non-RSC action errors now surface the Next.js-style action error path.
Maintainer review path
  1. packages/vinext/src/server/app-server-action-execution.ts for action redirect target rendering, cookie replay into the redirected GET render, and wrapper status selection.
  2. packages/vinext/src/server/app-browser-entry.ts and app-browser-action-result.ts for visible-route action POSTs, non-RSC error handling, and redirect Flight consumption.
  3. packages/vinext/src/server/app-segment-config.ts plus entries/app-rsc-entry.ts for runtime metadata threading.
  4. tests/app-server-action-execution.test.ts and tests/e2e/app-router/server-actions.spec.ts for the low-boundary and browser-level regression coverage.
Validation
  • vp env exec --node 24 ./scripts/run-nextjs-deploy-suite.sh /Users/nathan/Projects/vinext/.refs/nextjs-v16.2.6 --retries 0 -c 1 --debug test/e2e/app-dir/actions/app-action-node-middleware.test.ts
  • vp test run tests/app-server-action-execution.test.ts tests/app-browser-entry.test.ts tests/app-segment-config.test.ts tests/app-route-handler-response.test.ts tests/app-rsc-cache-busting.test.ts tests/entry-templates.test.ts
  • CI=1 PLAYWRIGHT_PROJECT=app-router npx playwright test tests/e2e/app-router/server-actions.spec.ts -g "server action redirects|stale child-route|cross-runtime|server action notFound|text/plain|invalid action response" --retries=0
  • vp check
  • Commit hooks reran format, lint/type checks, and knip --no-progress; the first commit hook also reran tests/entry-templates.test.ts.
Risk and compatibility
  • Runtime impact: action redirect POSTs now render the target App route internally when possible. External redirects and non-App targets still fall back to header-only 303 responses.
  • Router impact: stale child-route and cross-runtime redirect cases use wrapper status semantics to match the observable Next.js client contract.
  • Compatibility: no public API changes. Segment runtime is parsed only from supported Next.js values.
  • Main risk: vinext does not have a per-page action worker manifest, so stale visible-route action redirects are detected from route shape and runtime metadata rather than Next.js worker ownership data.
Non-goals
  • This does not add a Next.js-style action worker manifest.
  • This does not change middleware execution ordering.
  • This does not attempt to support unsupported server action encryption or revalidateTag cases outside the existing skipped upstream matrix.

References

Reference Why it matters
Next.js upstream test covering forwarded action redirect status Defines the failing observable behavior.
Next.js action forwarding helper Shows that forwarded actions are executed through the worker forwarding path.
Next.js worker selection Shows how Next.js decides which page worker owns an action.
Next.js redirected action render result Shows the redirect render path used for forwarded actions.
Next.js fetch action redirect status handling Shows how fetch action redirects are encoded for the client.

Server action redirects could fall back to hard HTTP redirects when the action POST represented stale visible-route state or a cross-runtime worker hop. That diverges from Next.js, where forwarded action redirects return an RSC wrapper response that the client can apply without treating the action as a failed network redirect.

The action pipeline now renders same-origin redirect targets into the POST response, preserves action cookies for the target render, and uses the wrapper status for forwarded, stale child-route, and cross-runtime redirect shapes. Browser action handling now posts to the visible route with the public next-action header and only runs RSC compatibility checks for real Flight responses.

Regression coverage ports the relevant Next.js action cases across helper tests and app-router E2E fixtures.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 23, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1582

commit: b0d2df8

Forwarded non-redirect actions could skip page rendering without preserving Set-Cookie or x-action-revalidated headers, and same-origin redirect rendering treated any App route match as a page render target. Redirect Flight responses also bypassed the RSC compatibility guard.

Gate inline redirect rendering to actual page routes, propagate forwarded action side-effect headers on the no-rerender path, and hard-navigate redirect responses when their Flight compatibility ID does not match the client. Text/plain action failures now accept charset-qualified content types.

Regression coverage exercises route-handler redirect fallback, forwarded cookie and revalidation propagation, redirect Flight compatibility fallback, and text/plain;charset=utf-8 action errors.
The zizmor online impostor-commit audit failed before auditing because the job token had no contents read scope. The audit needs to query GitHub tag and commit metadata for referenced actions such as voidzero-dev/setup-vp.

Grant contents: read to the zizmor job while keeping checkout credentials unpersisted and security-events scoped to the existing upload path.
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review May 23, 2026 15:28
Copilot AI review requested due to automatic review settings May 23, 2026 15:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Aligns Vinext App Router server-action behavior with Next.js for redirecting actions, especially when the visible route is stale or the action is effectively “forwarded”, by ensuring the client receives a consumable Flight wrapper (and correct wrapper-vs-redirect status semantics) and by improving client-side error/redirect handling.

Changes:

  • Server: render same-origin redirect targets into the action POST Flight response when possible, preserve cookies/revalidation headers, and select wrapper status semantics for forwarded/stale/cross-runtime redirects.
  • Browser: keep action POSTs on the visible route URL, send the public next-action header, improve handling of non-RSC action error responses, and hard-navigate when redirect Flight is incompatible.
  • Routing/metadata/tests: thread segment runtime into route config, refine root boundary derivation in the generated manifest, and add/extend unit + e2e fixtures/tests for the new Next.js-compat scenarios.

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/fixtures/app-basic/app/nextjs-compat/action-response-semantics/page.tsx Adds compat fixture page for action response semantics.
tests/fixtures/app-basic/app/nextjs-compat/action-response-semantics/not-found.tsx Adds compat not-found boundary for action notFound assertions.
tests/fixtures/app-basic/app/nextjs-compat/action-response-semantics/error.tsx Adds compat error boundary for surfaced action errors.
tests/fixtures/app-basic/app/nextjs-compat/action-response-semantics/client.tsx Client UI to trigger complete/redirect/notFound actions.
tests/fixtures/app-basic/app/nextjs-compat/action-response-semantics/actions.ts Server actions to drive semantics tests (return/redirect/notFound).
tests/fixtures/app-basic/app/nextjs-compat/action-redirect-cookies/target/page.tsx Target page asserts cookies merged and next-action stripped.
tests/fixtures/app-basic/app/nextjs-compat/action-redirect-cookies/page.tsx Source page action mutates cookies then redirects.
tests/fixtures/app-basic/app/nextjs-compat/action-forwarded-redirect/page.tsx Fixture page for forwarded redirect scenario.
tests/fixtures/app-basic/app/nextjs-compat/action-forwarded-redirect/other/page.tsx Fixture “other” page for stale child-route redirect test.
tests/fixtures/app-basic/app/nextjs-compat/action-forwarded-redirect/node/page.tsx Fixture node target page for cross-runtime redirect test.
tests/fixtures/app-basic/app/nextjs-compat/action-forwarded-redirect/edge/page.tsx Fixture edge page using cross-runtime client.
tests/fixtures/app-basic/app/nextjs-compat/action-forwarded-redirect/edge/other/page.tsx Fixture edge “other” page for cross-runtime stale route test.
tests/fixtures/app-basic/app/nextjs-compat/action-forwarded-redirect/edge/layout.tsx Sets runtime = "edge" for cross-runtime route metadata.
tests/fixtures/app-basic/app/nextjs-compat/action-forwarded-redirect/cross-runtime-client.tsx Client triggers delayed cross-runtime redirect action.
tests/fixtures/app-basic/app/nextjs-compat/action-forwarded-redirect/client.tsx Client triggers delayed forwarded redirect action.
tests/fixtures/app-basic/app/nextjs-compat/action-forwarded-redirect/actions.ts Redirecting server actions for forwarded/cross-runtime cases.
tests/fixtures/app-basic/app/action-redirect-test/redirect-form.tsx Updates redirect trigger from form submit to button click transition.
tests/entry-templates.test.ts Adds regression test for root boundary derivation without root page.
tests/e2e/app-router/server-actions.spec.ts Adds e2e coverage for action visible-route POSTs, redirects, stale/cross-runtime wrapper semantics, and invalid responses.
tests/e2e/app-router/nextjs-compat/actions-revalidate.spec.ts Adjusts POST URL matching for visible-route action requests.
tests/app-server-action-execution.test.ts Extends unit coverage for redirect rendering, cookie replay, forwarded wrapper semantics, and redirected Flight behavior.
tests/app-segment-config.test.ts Adds tests for segment runtime resolution/unknown runtime ignoring.
tests/app-rsc-cache-busting.test.ts Adds test ensuring server action POST URLs stay on visible route (strip hash).
tests/app-route-handler-response.test.ts Adds test for returned Set-Cookie normalization when overriding fallbacks.
tests/app-browser-entry.test.ts Adds unit tests for new action response parsing/compatibility/error behavior and next-action header inclusion.
packages/vinext/src/shims/error-boundary.tsx Formatting-only state object changes.
packages/vinext/src/server/app-server-action-execution.ts Core server-side redirect rendering, wrapper status selection, cookie replay into redirected GET render, soft tag threading, forwarded rerender suppression changes.
packages/vinext/src/server/app-segment-config.ts Parses and threads supported runtime values into effective segment config.
packages/vinext/src/server/app-rsc-cache-busting.ts Adds createServerActionRequestUrl() helper to keep action POSTs on visible route URL.
packages/vinext/src/server/app-route-handler-response.ts Normalizes returned Set-Cookie values by ensuring Path=/ when missing.
packages/vinext/src/server/app-browser-state.ts Adds public next-action header to action request state.
packages/vinext/src/server/app-browser-entry.ts Updates browser action dispatch to visible-route URLs, improves redirect Flight handling, non-RSC error surfacing, compatibility hard-nav logic, and 404 head metadata syncing.
packages/vinext/src/server/app-browser-action-result.ts Adds helpers for non-RSC action error extraction, compatibility checks, and HTTP fallback normalization.
packages/vinext/src/entries/app-rsc-manifest.ts Improves root boundary (notFound/forbidden/unauthorized/layout) derivation when no root page exists.
packages/vinext/src/entries/app-rsc-entry.ts Threads route runtime resolution into server action execution options.
.github/workflows/zizmor.yml Adds contents: read permission for zizmor workflow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/vinext/src/server/app-server-action-execution.ts
Comment thread packages/vinext/src/server/app-browser-entry.ts
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

@codex review

Forwarded action redirects treated a missing segment runtime as incomparable instead of the implicit Node runtime. That let implicit Node to explicit Edge redirects use 303 semantics even though they cross runtime boundaries and need the wrapper response path.

Server action dispatch also left action-owned HTTP fallback head metadata in place until a response reached the status sync path. Invalid non-RSC responses could throw first and leave stale noindex metadata behind.

Normalize null route runtimes to nodejs at the comparison boundary, clear action fallback head metadata before each action fetch, and cover both regressions. The redirect render cookie replay now reuses the existing request cookie parser instead of carrying a local parser clone.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12f041d28a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/vinext/src/server/app-server-action-execution.ts
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.

Server actions: cookies merging, multiple-submit dedup, and network-error catching

2 participants