Skip to content

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

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

fix(app-router): preserve forwarded action redirect wrappers#1582
NathanDrake2406 wants to merge 13 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.

@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: 4f4bbbd

@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

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
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review

This is a substantial and well-structured PR that closes a meaningful parity gap with Next.js server-action redirect behavior. The core design — rendering the redirect target inline during the action POST and using wrapper status semantics for forwarded/stale/cross-runtime cases — is sound and well-motivated by the upstream Next.js references.

Test coverage is strong: the unit tests for app-server-action-execution are thorough (redirect rendering, cookie replay, context lifecycle, forwarded/stale/cross-runtime wrapping), and the Playwright E2E tests cover the key user-visible scenarios. Good provenance discipline with // Ported from Next.js: annotations.

The main areas where I have feedback are: (1) a potential cookie value round-trip corruption in the redirect render path, (2) an unexplained branching condition in the browser action dispatch, (3) the ACTION_FORWARDED_HEADER removal from the progressive action path, and (4) a few minor clarity items. See inline comments.

Comment thread packages/vinext/src/server/app-server-action-execution.ts Outdated
Comment thread packages/vinext/src/server/app-browser-entry.ts Outdated
Comment thread packages/vinext/src/server/app-server-action-execution.ts
Comment thread packages/vinext/src/server/app-server-action-execution.ts
Comment thread packages/vinext/src/entries/app-rsc-manifest.ts Outdated
Comment thread packages/vinext/src/server/app-server-action-execution.ts Outdated
Comment thread packages/vinext/src/server/app-browser-entry.ts
Comment thread packages/vinext/src/server/app-server-action-execution.ts
Comment thread packages/vinext/src/server/app-browser-entry.ts Outdated
Comment thread packages/vinext/src/server/app-server-action-execution.ts
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 26, 2026

Review submitted. Here's a summary of the feedback I posted:

Overall assessment: Strong PR with good design, thorough test coverage, and clear upstream references. The core approach of rendering redirect targets inline during the action POST is sound.

Key feedback points:

  1. Cookie value round-trip corruption risk (app-server-action-execution.ts:312) — The decodeURIComponent/encodeURIComponent round-trip in applySetCookieMutationsToRequestCookieHeader can corrupt cookie values containing raw % characters that aren't valid percent-escape sequences. Low practical risk since vinext's own cookies().set() uses standard encoding, but worth acknowledging.

  2. Unexplained args.length branching (app-browser-entry.ts:1331) — The distinction between programmatic actions (throw redirect error) and form actions (suspend with pending promise) is subtle and critical. Suggested adding a comment explaining the React-specific reasoning.

  3. Subtle actionStatus === 200 guard change (app-server-action-execution.ts:998) — The skip-rerender condition now requires actionStatus === 200 in addition to no-revalidation. This is likely correct (HTTP fallback actions need the rerender for boundary display), but it's an undocumented behavioral change.

  4. ACTION_FORWARDED_HEADER guard removal from progressive path (app-server-action-execution.ts:646) — Previously rejected forwarded progressive actions; now processes them fully. The header isn't referenced in the progressive path after removal.

  5. Minor items: indexOf redundancy in rootRouteBoundaryPath, legacy cookie date format edge case, suggestion comments on createPendingServerActionRedirectPromise and redirect header initialization ordering.

  6. Positive callouts: Good response cloning before error detection, nice createServerActionRscResponse pattern for deferred context cleanup.

github run

@NathanDrake2406 NathanDrake2406 force-pushed the nathan/app-action-node-middleware branch from b0d2df8 to ce25702 Compare May 28, 2026 09:38
@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

Addressed the review feedback from @ask-bonk:

  1. Cookie value round-trip corruption — Removed the unnecessary encodeURIComponent call in applySetCookieMutationsToRequestCookieHeader. Cookie header values are raw, not URL-encoded, and readSetCookieNameValue already decodes from Set-Cookie format. The encode/decode round-trip is now eliminated.

  2. args.length branching — Added a comment documenting the React-specific distinction: programmatic actions (no args) throw a redirect error, while form actions (args present) suspend via a pending promise.

  3. actionStatus === 200 guard — Added a comment explaining that non-200 HTTP fallback statuses (e.g., 404 from notFound()) need the page rerender to display error boundaries, so only 200 status actions skip the rerender.

  4. ACTION_FORWARDED_HEADER in progressive path — Added a comment documenting that progressive form submissions (multipart without actionId) don't carry a forwarded-action header. They route to the visible page directly and can't be redirected cross-runtime.

  5. Minor items:

    • Replaced indexOf with findIndex in rootRouteBoundaryPath for clarity
    • Relaxed expired-cookie date regex to accept Thu, 1 Jan 1970 (without leading zero)
    • Added explanatory comment to createPendingServerActionRedirectPromise
    • Removed unused navigateClientSide import in app-browser-entry.ts
    • Updated edge-runtime redirect test status expectation to 303 (same-runtime, non-forwarded)

All 262 targeted tests pass cleanly. Rebased on current upstream/main.

@NathanDrake2406 NathanDrake2406 requested a review from Copilot May 28, 2026 09:40
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

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

Comment thread packages/vinext/src/server/app-browser-entry.ts
Comment thread packages/vinext/src/server/app-browser-entry.ts
Comment thread packages/vinext/src/server/app-route-handler-response.ts Outdated
Comment thread packages/vinext/src/server/app-segment-config.ts
Comment thread packages/vinext/src/server/app-segment-config.ts Outdated
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review

This is a well-designed PR that closes a real parity gap with Next.js server-action redirect behavior. The core approach — rendering the redirect target inline during the action POST and using wrapper status semantics for forwarded/stale/cross-runtime cases — is sound, well-referenced to upstream Next.js code, and backed by good test coverage at both unit and E2E levels.

The previous review from @ask-bonk was thorough and most feedback was addressed. I have one material issue (the null runtime normalization that was claimed fixed but appears missing), a couple of minor observations, and some positive callouts.

Summary

  • 1 issue — implicit Node → explicit Edge runtime comparison still broken (see inline)
  • 1 nit — duplicate runtime assignment in segment config loop
  • 1 observationrootRouteBoundaryPath findIndex always returns 0
  • Good overall design, test discipline, and upstream provenance annotations

Comment thread packages/vinext/src/server/app-server-action-execution.ts Outdated
Comment thread packages/vinext/src/server/app-segment-config.ts
Comment thread packages/vinext/src/entries/app-rsc-manifest.ts Outdated
Comment thread packages/vinext/src/server/app-server-action-execution.ts Outdated
Comment thread packages/vinext/src/server/app-browser-entry.ts Outdated
Comment thread packages/vinext/src/server/app-browser-entry.ts
Comment thread packages/vinext/src/server/app-server-action-execution.ts
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

Review submitted. Here's a summary of the feedback I posted:

Overall assessment: Well-designed PR that closes a meaningful Next.js parity gap. The core approach of rendering redirect targets inline during the action POST and using wrapper status semantics is sound, well-referenced to upstream Next.js, and has strong test coverage.

Key feedback:

  1. Implicit Node → Edge runtime comparison is still broken (app-server-action-execution.ts:568) — The author claimed to have fixed null runtime normalization per the previous review, but the fix doesn't appear in the final code. When currentRuntime is null (implicit Node, no runtime export) and targetRuntime is "edge", shouldUseForwardedActionRedirectStatus returns false instead of true. The existing cross-runtime test only covers Edge → Node (where currentRuntime="edge" is not null). Suggested normalizing null to "nodejs" before comparing and adding a unit test for the reverse direction.

  2. Comment/code mismatch on args.length branching (app-browser-entry.ts:1338) — The comment says "programmatic actions (args.length === 0) throw" and "form actions (args.length !== 0) suspend", but the code does the opposite: if (args.length !== 0) throws. The parenthetical mapping in the comment is swapped.

  3. Duplicate runtime assignment (app-segment-config.ts:113) — config.runtime is set on both line 113 and line 163 within the same loop iteration, both guarded by isRouteSegmentRuntime. The second always overwrites the first with the same value.

  4. rootRouteBoundaryPath findIndex always returns 0rootPosition is layoutTreePositions[0], so findIndex always finds it at index 0. Not blocking, just noting.

  5. Minor cookie value encoding asymmetryreadSetCookieNameValue decodes values while original Cookie header values stay raw, creating a subtle mixed-encoding state in the redirect render request.

  6. Positive callouts — Good stale meta clearing in syncServerActionHttpFallbackHead, nice createServerActionRscResponse pattern for deferred context cleanup.

github run

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review

This is a well-executed PR that closes a meaningful Next.js parity gap for server-action redirects. The core design — rendering the redirect target inline during the action POST, using wrapper status semantics for forwarded/stale/cross-runtime cases, and deferring context cleanup until stream consumption — is sound and well-motivated by the upstream references.

The previous review cycles have been productive; the runtime normalization fix, duplicate segment config cleanup, and cookie encoding fixes are all confirmed in the final code. Test coverage is strong at both unit and E2E levels.

I have a few remaining items below — one stale/misleading comment, one potential correctness issue, and a few observations.

Summary

  • 1 nit — misleading comment on cookie value encoding (no behavioral impact)
  • 1 questionreadInvalidServerActionResponseError rejects 200 OK non-RSC responses as errors
  • 1 observationresolveServerActionRedirectLocation is now dead code in production
  • 1 minor — relative URL resolution includes search params in base, which is spec-correct but could surprise

Comment thread packages/vinext/src/server/app-server-action-execution.ts Outdated
Comment thread packages/vinext/src/server/app-browser-action-result.ts
Comment thread packages/vinext/src/server/app-browser-entry.ts
Comment thread packages/vinext/src/server/app-server-action-execution.ts
Comment thread packages/vinext/src/server/app-browser-entry.ts
Comment thread packages/vinext/src/server/app-server-action-execution.ts
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

Review submitted. Here's a summary of the feedback I posted:

Overall assessment: Well-executed PR that closes a meaningful Next.js parity gap. The core approach of rendering redirect targets inline during the action POST, wrapper status semantics for forwarded/stale/cross-runtime cases, and deferred context cleanup via createServerActionRscResponse are all sound. Previous review feedback has been addressed — runtime normalization, duplicate segment config, and cookie encoding fixes are all confirmed in the final code.

Key feedback:

  1. Stale comment on cookie value encoding (app-server-action-execution.ts:315-317) — After removing decodeURIComponent, the comment still says the value "has already been decoded from the Set-Cookie header's URL-encoded form." readSetCookieNameValue does no decoding — it slices verbatim. Suggested rewording.

  2. readInvalidServerActionResponseError rejects all non-RSC responses (app-browser-action-result.ts:137) — Including 200 OK responses with non-RSC content types. This matches Next.js behavior but is a strict gate worth being aware of.

  3. Forwarded action + notFound() skip-rerender interaction (app-server-action-execution.ts:1021) — A forwarded action that throws notFound() skips the page rerender (since actionWasForwarded takes priority). Confirmed this works because the client-side normalizeServerActionThrownValue converts the thrown value into a NEXT_HTTP_ERROR_FALLBACK digest that React's error boundary catches.

  4. Positive callouts: Good defensive ordering in the browser entry (non-RSC redirect → hard nav check runs after invalid response error check), and well-designed deferred cleanup pattern where setHeadersContext for the redirect target survives until RSC streaming completes.

github run

@NathanDrake2406 NathanDrake2406 force-pushed the nathan/app-action-node-middleware branch from baecc5e to 59e3053 Compare May 28, 2026 12:01
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review

This is a well-executed PR that closes a meaningful Next.js parity gap for server-action redirect behavior. The core design is sound: rendering the redirect target inline during the action POST, using wrapper status semantics (200 vs 303) for forwarded/stale/cross-runtime cases, and deferring clearRequestContext until the RSC stream is fully consumed via createServerActionRscResponse.

All feedback from the three previous review cycles has been addressed: runtime normalization, duplicate segment config removal, cookie encoding fix, args.length branching removal, stale comments, trailing semicolon handling, and default redirect type. The code is in good shape.

Test coverage is strong — 455+ new lines in the server action unit tests, 183 new lines in E2E, and good provenance discipline with // Ported from Next.js: annotations. The test fixtures are well-structured with appropriate separation of server/client concerns.

Minor items

I have two minor observations and one comment-level nit. None are blocking.


1. readInvalidServerActionResponseError is a strict non-RSC gate including 200 OK.

This function (in app-browser-action-result.ts:121-138) returns an error for any response that isn't text/x-component and doesn't have a redirect header — including a 200 OK with application/json. This matches Next.js behavior (their server-action reducer rejects non-Flight responses regardless of status), so it's correct for parity. Just flagging for awareness: if a middleware or CDN ever rewrites an action response's content-type, this will throw rather than attempt to process the body. The existing readInvalidServerActionResponseError name makes this behavior reasonably discoverable.

2. isStaleChildSiblingRouteRedirect — naming vs semantics.

The function detects redirects where the current path is deeper than the target and they share a partial prefix but diverge before the target ends. The name says "child sibling" but it more precisely detects "divergent cousin" redirects (target diverges from an ancestor of current). The behavior is correct — these are exactly the cases where the visible-route worker wouldn't own the target page in Next.js's multi-worker model. The name is fine for now but could be clarified if the function's scope ever changes.

3. Comment precision on shouldSkipPageRendering.

The comment at app-server-action-execution.ts:1016-1019 says "Only skip when the action status is 200 and no revalidation side-effects occurred" but doesn't mention the actionWasForwarded case (line 1021) which also skips unconditionally. The comment was added per a previous review — it just needs a small amendment to cover the forwarded case for completeness.

Comment thread packages/vinext/src/server/app-server-action-execution.ts
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

Review submitted. Here's a summary of what I found:

Overall assessment: Approved. This is a well-executed PR that closes a meaningful Next.js parity gap. All feedback from three previous review cycles has been addressed and verified in the current code.

What the PR does:

  • Renders same-origin action redirect targets inline during the action POST (instead of returning an empty redirect response)
  • Uses wrapper status semantics (200 vs 303) for forwarded, stale child-route, and cross-runtime redirect cases
  • Defers clearRequestContext until RSC stream consumption via createServerActionRscResponse
  • Fixes the browser action dispatch to POST to the visible route URL and handle inline Flight redirect payloads

What I verified:

  • All 7 previously flagged issues (runtime normalization, duplicate segment config, cookie encoding, args.length branching, stale comments, trailing semicolons, default redirect type) are confirmed fixed
  • The isStaleChildSiblingRouteRedirect and shouldUseForwardedActionRedirectStatus logic is correct
  • URL resolution is consistent across server and browser implementations
  • Cookie mutation replay for redirect render requests is sound
  • Test coverage is strong (455+ new unit test lines, 183 E2E lines, good Next.js test provenance)

One minor suggestion posted: Expanding the shouldSkipPageRendering comment to also mention the actionWasForwarded branch for completeness.

github run

@NathanDrake2406 NathanDrake2406 force-pushed the nathan/app-action-node-middleware branch from d0449c6 to 878dfab Compare May 28, 2026 13:09
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review

This is a well-executed PR that closes a meaningful Next.js parity gap for server-action redirect behavior. The core architectural shift — rendering the redirect target inline during the action POST instead of returning an empty redirect and forcing a second browser navigation — is sound and well-motivated by the upstream Next.js references.

What I verified

Server-side redirect rendering (app-server-action-execution.ts):

  • The resolveInternalActionRedirectTargetmatchRoutecanRenderActionRedirectTargetcreateActionRedirectRenderRequest → render pipeline is clean and has correct fallbacks (external URLs → 303 header-only, non-page routes → 303 header-only).
  • createServerActionRscResponse correctly defers clearRequestContext via deferUntilStreamConsumed for stream bodies while clearing immediately for non-stream bodies. This prevents the AsyncLocalStorage race that existed before.
  • Cookie replay via applySetCookieMutationsToRequestCookieHeader correctly merges Set-Cookie mutations into the redirect render request's Cookie header, with proper handling of expired cookies (max-age=0 and epoch expires). The readSetCookieNameValue extraction is now verbatim (no encode/decode asymmetry).
  • The shouldUseForwardedActionRedirectStatus logic correctly handles all four cases (forwarded, ancestor, stale-sibling, cross-runtime) and the normalizeRuntime helper resolves null"nodejs" so implicit-Node → explicit-Edge is detected.
  • Set-Cookie and x-action-revalidated headers are now properly emitted on the skip-rendering path, fixing the forwarded action side-effect loss bug.

Browser-side action dispatch (app-browser-entry.ts):

  • The switch from createRscRequestUrl to createServerActionRequestUrl for action POSTs correctly targets the visible route URL, matching Next.js behavior.
  • resolveActionRedirectTarget properly handles absolute, relative, and slash-prefixed redirects with correct cross-origin detection and hard-navigation fallback.
  • The RSC compatibility check gating (shouldCheckRscCompatibilityForServerActionResponse) correctly prevents non-RSC error responses from being misclassified as deployment skew.
  • The 303→200 status normalization before createFromFetch is necessary because React's Flight client rejects non-2xx responses.
  • The ServerActionRedirectError with handled = true and proper NEXT_REDIRECT digest matches the expected React error boundary integration.

Segment config and manifest (app-segment-config.ts, app-rsc-manifest.ts):

  • The runtime type narrowing to the union type and isRouteSegmentRuntime guard are clean. The duplicate assignment was correctly removed.
  • The findRootBoundaryRoute fallback for apps without a root page is a good fix — it prevents boundary loss when app/not-found.tsx exists but app/page.tsx doesn't.

Test coverage:

  • 455+ new unit test lines covering redirect rendering, cookie replay, deferred context cleanup, forwarded/stale/cross-runtime wrapper semantics, and HTTP fallback status.
  • 183 new E2E lines covering text/plain error propagation, notFound boundary rendering, single-POST redirect completion, cookie merging, stale-route wrapper status, and cross-runtime wrapper status.
  • Good provenance discipline with // Ported from Next.js: annotations throughout.

Issues found

I found two minor items (both inline below). Neither is blocking.

Good work overall — the design is clean, the fallback paths are correct, and the test coverage is thorough.

Comment thread packages/vinext/src/server/app-server-action-execution.ts
Comment thread packages/vinext/src/server/app-server-action-execution.ts
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

Review submitted. Here's a summary of the feedback I posted:

Overall assessment: Approved. This is a well-executed PR that closes a meaningful Next.js parity gap for server-action redirect behavior. The core architectural shift — rendering the redirect target inline during the action POST instead of returning an empty redirect — is sound, well-referenced to upstream Next.js, and has thorough test coverage.

What I verified:

  • Server-side redirect rendering pipeline with correct fallback paths (external URLs → 303, non-page routes → 303)
  • Deferred clearRequestContext via deferUntilStreamConsumed prevents AsyncLocalStorage race
  • Cookie replay, runtime normalization (null"nodejs"), and forwarded action side-effect preservation
  • Browser-side visible-route POST URL, RSC compatibility gating, 303→200 normalization, and ServerActionRedirectError integration
  • Segment config runtime type narrowing, root boundary fallback for apps without root page
  • 455+ new unit test lines and 183 new E2E lines with good provenance annotations

Two minor inline comments posted (neither blocking):

  1. isStaleChildSiblingRouteRedirect — the depth inequality (currentSegments.length > targetSegments.length) means it only detects deeper-to-shallower sibling redirects, not same-depth siblings. This is likely intentional but worth a clarifying comment.
  2. shouldSkipPageRendering comment — the existing comment documents the actionStatus === 200 guard but doesn't mention the actionWasForwarded branch that unconditionally skips regardless of status. Suggested expanding the comment to cover both branches.

github run

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.
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.
- Remove encodeURIComponent round-trip in applySetCookieMutationsToRequestCookieHeader;
  Cookie header values are raw, not URL-encoded
- Guard segment runtime fallback with isRouteSegmentRuntime to ignore unknown values
- Replace indexOf with findIndex in rootRouteBoundaryPath for clarity
- Relax expired-cookie date regex to accept 'Thu, 1 Jan 1970' without leading zero
- Document args.length branching for programmatic vs form action redirect semantics
- Document actionStatus === 200 guard on skip-page-render condition
- Document why progressive action path omits forwarded-action header check
- Comment createPendingServerActionRedirectPromise per React suspension semantics
- Fix edge-runtime redirect test to expect 303 (same-runtime, non-forwarded)
- Remove unused navigateClientSide import after redirect path refactor
The root layout position is always layoutTreePositions[0], so indexOf /
findIndex was a no-op dance. The boundary at position 0 is just
boundaryPaths[0].
@NathanDrake2406 NathanDrake2406 force-pushed the nathan/app-action-node-middleware branch from 878dfab to f9d475d Compare May 29, 2026 05:17
…y, broaden expired cookie regex, fix redirect dedup test status
@james-elicx
Copy link
Copy Markdown
Member

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review

This is a well-designed PR that closes a meaningful Next.js parity gap. The core shift — rendering redirect targets inline during the action POST and returning a Flight payload with redirect control headers — is the right architecture. The deferred cleanup pattern (createServerActionRscResponse + deferUntilStreamConsumed), cookie replay into the synthetic redirect render request, and wrapper status selection logic are all clean and well-tested.

This has been through five review cycles and all prior feedback appears addressed. I found one minor correctness concern, one test coverage gap, and a couple of observations. Nothing blocking.

Summary

  • 1 minor issuereadInvalidServerActionResponseError guards ordering relative to revalidation parsing
  • 1 test gap — no coverage for relative redirect URLs in the redirect rendering path
  • 2 observations — duplicated URL resolution logic, E2E timing dependency

What I verified

  • Redirect rendering pipeline: resolveInternalActionRedirectTargetmatchRoutecanRenderActionRedirectTargetcreateActionRedirectRenderRequest → render → createServerActionRscResponse. Fallbacks to 303 header-only responses for external, unmatched, and route-handler targets are correct.
  • Deferred cleanup: deferUntilStreamConsumed correctly hooks flush, cancel, and reader error, with a once() guard. The non-stream path calls clearRequestContext synchronously.
  • Cookie replay: applySetCookieMutationsToRequestCookieHeader correctly merges Set-Cookie mutations, handles max-age=0 / epoch expires deletions, and stores values verbatim (previous decode/encode asymmetry fixed).
  • Runtime normalization: normalizeRuntime collapses null"nodejs" and "experimental-edge""edge". All four shouldUseForwardedActionRedirectStatus cases (forwarded, ancestor, stale-sibling, cross-runtime) are tested.
  • Browser-side dispatch: visible-route POST URLs, next-action header, 303→200 rewrite for createFromFetch, response cloning before error detection, and ServerActionRedirectError with handled: true are all sound.
  • Manifest changes: findRootBoundaryRoute fallback for apps without a root page, rootRouteLayoutPaths filtering to root-position layouts, and rootRouteBoundaryPath boundary selection are correct.
  • Test coverage: 455+ new unit test lines, 183 new E2E lines, good provenance annotations. The E2E tests cover the key user-visible scenarios including cookie merging, stale-route wrapper status, and cross-runtime wrapper status.

Comment thread packages/vinext/src/server/app-browser-entry.ts
Comment thread packages/vinext/src/server/app-server-action-execution.ts
Comment thread packages/vinext/src/server/app-server-action-execution.ts
Comment thread packages/vinext/src/server/app-browser-entry.ts
Comment thread tests/e2e/app-router/server-actions.spec.ts
Comment thread packages/vinext/src/server/app-server-action-execution.ts
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 29, 2026

Review submitted. Here's a summary of what I posted:

Overall assessment: Well-designed PR that closes a meaningful Next.js parity gap. The core architecture — rendering redirect targets inline during the action POST and using wrapper status semantics — is sound. All prior review feedback (5 cycles) has been addressed. No blocking issues found.

What I verified:

  • Redirect rendering pipeline with correct fallbacks to 303 for external/unmatched/route-handler targets
  • Deferred clearRequestContext via deferUntilStreamConsumed with proper once() guard
  • Cookie replay merging Set-Cookie mutations verbatim (decode/encode asymmetry fixed)
  • Runtime normalization (null"nodejs", "experimental-edge""edge") across all four wrapper status cases
  • Browser-side visible-route POST URLs, next-action header, 303→200 rewrite, response cloning, and ServerActionRedirectError
  • Manifest changes for root boundary discovery without a root page

Inline comments posted (6 total, none blocking):

  1. Confirmed response clone control flow is safe in the browser action dispatch
  2. Flagged that buildServerActionPageTags might want verification for intercepted routes' routeSegments
  3. Suggested a clarifying comment on isStaleChildSiblingRouteRedirect's intentional depth inequality
  4. Noted duplicated URL resolution logic between browser and server
  5. Flagged potential E2E timing sensitivity in the stale child-route redirect test
  6. Positive callout on the redirect render context setup and deferred cleanup

github run

@NathanDrake2406
Copy link
Copy Markdown
Contributor Author

All review feedback from @ask-bonk has been addressed. Summary of fixes across review cycles:

Code changes in this PR:

  1. Cookie value encoding — Removed decodeURIComponent/encodeURIComponent round-trip in readSetCookieNameValue to prevent cookie value corruption. Values are now passed verbatim.
  2. args.length branching — We always throw ServerActionRedirectError for both programmatic and form-based action redirects. SPA navigations run asynchronously via renderNavigationPayload; returning a pending promise would block state updates.
  3. Skip-rerender condition — Added comment documenting that actionWasForwarded takes unconditional priority and that only actionStatus === 200 non-forwarded actions skip the page rerender (HTTP fallback actions like notFound() need the rerender for boundary display).
  4. ACTION_FORWARDED_HEADER in progressive path — Progressive form submissions don't carry a forwarded-action header; they route to the visible page directly. This matches upstream Next.js behavior.
  5. Runtime normalization — null route runtimes are normalized to "nodejs" before comparing, so implicit Node -> explicit Edge redirects correctly use the wrapper 200 response path.
  6. Duplicate runtime assignment — Removed the duplicate config.runtime assignment from the end of the segment config reduction loop.
  7. Expired cookie regex — Broadened isExpiredSetCookie regex to match both RFC 6265 space format and legacy RFC 2109 hyphen format (Thu, 01-Jan-1970).
  8. Stale comment on cookie encoding — Updated comment to reflect that values are extracted verbatim without decoding.
  9. Default redirect type — Changed from "replace" to "push" to match Next.js server-action-reducer.ts:370.
  10. isStaleChildSiblingRouteRedirect depth guard — Added clarifying comment that currentSegments.length > targetSegments.length is intentional (same-depth siblings share the same page worker in the Next.js model, so only deeper-to-shallower redirects can be stale).
  11. Scroll intent on action redirects — Captured beginAppRouterScrollIntent return value and passed it through renderNavigationPayload so scroll-to-top works correctly.
  12. shouldSkipPageRendering comment — Expanded to document both the actionWasForwarded unconditional branch and the actionStatus === 200 guard.
  13. E2E timing — Increased fixture delay from 1s to 3s to avoid race between client-side navigation and delayed action redirect in slow CI environments.

Observations acknowledged (no code change needed):

  • Response clone control flow confirmed safe.
  • readInvalidServerActionResponseError rejecting non-RSC responses matches Next.js behavior.
  • Relative URL resolution including base search params is spec-correct.
  • Duplicated URL resolution logic between browser and server noted for future refactoring.
  • buildServerActionPageTags observation on intercepted routes noted (low risk, cache tags are advisory).

All 57 unit tests in app-server-action-execution.test.ts pass cleanly.

@james-elicx
Copy link
Copy Markdown
Member

/bigbonk re-review for issues and parity

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Re-review: Issues and Parity

Thorough re-review with focus on correctness, Next.js parity, and edge cases. This is a well-designed PR that closes a real gap. The core architecture — rendering redirect targets inline during the action POST, wrapper status semantics, and deferred context cleanup — is sound. Five prior review cycles have addressed the major issues.

I found a few remaining items. One is a minor behavioral concern, the others are observations and nits.

Summary

  • 1 minor issueresolveServerActionRedirectLocation is now dead production code
  • 1 parity questionreadInvalidServerActionResponseError treats 200 OK with non-RSC content-type as an error (matches Next.js, but worth documenting)
  • 2 nits — test variable shadowing, E2E timing fragility
  • Code quality — clean, well-referenced to upstream, strong test coverage (456+ unit test lines, 183 E2E lines)

Overall the code is in good shape. The prior review feedback has been thoroughly addressed.

@@ -100,6 +105,62 @@ export function resolveServerActionRedirectLocation(options: {
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: resolveServerActionRedirectLocation is no longer called from any production code path — the browser entry now uses resolveActionRedirectTarget (in app-browser-entry.ts:630). It's still exported and tested in app-browser-entry.test.ts, but it's dead code in production.

Not blocking — it could be useful as a utility for future callers, and keeping the tests ensures behavioral documentation. But if you want to trim dead exports, this is a candidate.

Promise.resolve(flightResponse),
{ temporaryReferences },
);
syncServerActionHttpFallbackHead(fetchResponse.status);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Observation: syncServerActionHttpFallbackHead(fetchResponse.status) runs after createFromFetch parses the Flight payload. If createFromFetch throws (e.g., malformed Flight data), the head sync never runs, meaning a stale noindex from a prior action could persist. However, this is mitigated by the syncServerActionHttpFallbackHead(null) call at the top of the callback (line 1246), which clears any stale meta before the fetch. So the only scenario where stale meta could persist is if a previous action set 404 meta and the current action's createFromFetch throws — but the 404 meta was already cleared at line 1246. Safe.

Good defensive pattern.

headers: withoutRscBodyHeaders(redirectHeaders),
});
}
const currentMatch = options.matchRoute(options.cleanPathname);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Subtle: options.matchRoute(options.cleanPathname) is called to get currentMatch for the wrapper status decision, but this is after the navigation context was already switched to the redirect target (lines 1047-1054). At this point the cleanPathname still correctly refers to the original action's pathname since it's a parameter, not a mutable state. But the headers context and navigation context are now pointing at the redirect target.

This is fine because shouldUseForwardedActionRedirectStatus only reads the currentRoute and targetRoute objects (which are immutable route descriptors), not any ambient context. Just noting the ordering for future readers: line 1037 must stay before any code that would mutate options.cleanPathname (currently impossible since it's a readonly option, but worth being aware of if the type ever changes).

return currentRuntime !== targetRuntime;
}

function canRenderActionRedirectTarget(route: AppServerActionRoute): boolean {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Correctness question: canRenderActionRedirectTarget checks "page" in route and requires page to be truthy. But the AppServerActionRoute type definition (line 88) has page?: unknown — it's optional. When page is absent from the object entirely, "page" in route returns false, and the function falls through to return true (line 643).

This means if a route doesn't have a page property at all (not null, not undefined, just absent), it's treated as renderable. Is this intentional? The generated route objects should always have page when they're page routes, but this feels like it could surprise if a manually constructed route object (e.g., in tests) omits page.

The existing tests pass explicit page values, so this isn't a current issue. Just a defensive coding observation.

await page.goto(`${BASE}/nextjs-compat/action-forwarded-redirect`);
await waitForAppRouterHydration(page);
await page.click("#run-forwarded-redirect");
await page.click("#go-forwarded-redirect-other");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The stale child-route test relies on a 3-second setTimeout in the fixture's onClick handler to allow the navigation to /other to complete before the action redirect fires. The toPass({ timeout: 10_000 }) polling on the assertion (line 249) helps, but the fundamental timing assumption is that clicking #go-forwarded-redirect-other immediately after #run-forwarded-redirect always completes the SPA navigation within 3 seconds.

In slow CI environments with cold compilation, the initial click might trigger module compilation that consumes part of that 3-second window. The 3-second delay (increased from 1s per review feedback) gives reasonable margin, but if this test starts flaking in CI, this is the root cause. Consider either:

  • Using a page.waitForURL between the two clicks to ensure navigation completed
  • Having the fixture signal readiness via a DOM element rather than relying on a fixed delay

// Only deeper-to-shallower redirects can be stale in the Next.js worker
// model (same-depth siblings share the same page worker). The depth guard
// ensures we don't misclassify same-level redirects.
if (targetSegments.length === 0 || currentSegments.length <= targetSegments.length) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The depth guard comment is clear. One edge case to be aware of: isStaleChildSiblingRouteRedirect("/a/b", "/a/b/c/d") where target is shallower than current — this correctly returns true (commonPrefixLength = 2, targetSegments.length = 2, so commonPrefixLength < targetSegments.length is false... wait, actually this returns false because commonPrefixLength === targetSegments.length).

Let me trace through: target = /a/b (segments: ["a", "b"]), current = /a/b/c/d (segments: ["a", "b", "c", "d"]). targetSegments.length = 2, currentSegments.length = 4, so the depth guard passes. commonPrefixLength loops to 2 (both segments match). Then commonPrefixLength > 0 && commonPrefixLength < targetSegments.length2 > 0 && 2 < 2false.

This means a redirect from a deeper path to a parent-sibling path (e.g., /a/b/c/d/a/b) is NOT detected as stale. But that's correct — /a/b is an ancestor of /a/b/c/d, and that case is handled by isAncestorRouteRedirect at line 629 instead. The two functions partition the space cleanly. Good.

return null;
}
const statusHeader = response.headers.get(ACTION_REDIRECT_STATUS_HEADER);
const status = statusHeader ? parseInt(statusHeader, 10) : 307;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: the default status is 307 when the header is missing, while ACTION_REDIRECT_TYPE_HEADER defaults to "push" (line 662). In ServerActionRedirectError (line 680), the digest format includes the status: NEXT_REDIRECT;push;/path;307;. If a future consumer of this digest expects specific status semantics (e.g., 307 = preserve method, 303 = change to GET), the default of 307 may be incorrect for action redirects which should generally use 303 (change POST to GET). However, since this status is only used in the digest for React error boundary matching and not for actual HTTP semantics, it's fine in practice.

fallbackPath: string | null | undefined,
): string | null {
if (!route) return null;
if (route.pattern === "/") return fallbackPath ?? null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The boundaryPaths?.[0] fallback for non-root routes assumes that boundaryPaths (which is route.notFoundPaths, route.forbiddenPaths, etc.) is ordered with the root-level boundary first. This is true if boundary paths are ordered from outermost layout to innermost, which matches the layout tree scanning order in the router. If the ordering convention ever changes, this indexing would silently pick the wrong boundary.

Not blocking — the current scanner produces correctly ordered arrays.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 31, 2026

Review submitted. Here's a summary of the feedback I posted:

Overall assessment: Well-designed PR that closes a meaningful Next.js parity gap. The core architecture -- rendering redirect targets inline during the action POST, wrapper status semantics for forwarded/stale/cross-runtime cases, and deferred context cleanup via createServerActionRscResponse -- is sound. All prior review feedback (5 cycles) has been addressed.

Key feedback points:

  1. Dead production code (app-browser-action-result.ts:106) -- resolveServerActionRedirectLocation is no longer called from production code paths after the switch to resolveActionRedirectTarget. Still tested and exported, but dead in production.

  2. canRenderActionRedirectTarget defensive concern (app-server-action-execution.ts:640) -- When page is absent from the route object entirely (not null, just missing), the function falls through to return true, treating it as renderable. Not a current issue since generated routes always have page, but could surprise with manually constructed route objects.

  3. E2E timing fragility (server-actions.spec.ts:246) -- The stale child-route test relies on a 3-second setTimeout delay that could be tight in slow CI with cold compilation. Suggested using page.waitForURL or DOM readiness signals instead of fixed delays.

  4. Positive observations:

    • Clean partition between isAncestorRouteRedirect and isStaleChildSiblingRouteRedirect -- they cover complementary cases without overlap
    • Defensive syncServerActionHttpFallbackHead(null) at the top of the action callback correctly prevents stale noindex meta from persisting
    • The currentMatch ordering relative to context switches is safe because cleanPathname is a readonly option parameter
    • boundaryPaths?.[0] indexing in rootRouteBoundaryPath works correctly with current scanner ordering

github run

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

3 participants