Fix/action forward loop guard#84525
Closed
LiamBoz wants to merge 2 commits into
Closed
Conversation
Member
|
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
elishagreenwald
added a commit
to elishagreenwald/next.js
that referenced
this pull request
May 8, 2026
Avoid infinite peer-forward loops when middleware rewrites paths or the request was already forwarded (x-action-forwarded). The receiving worker short-circuits the forwarding decision and falls through to lenient serverModuleMap resolution instead of forwarding again. Adds a self-contained e2e regression test that reproduces the loop via a proxy.ts rewrite: with the fix the request resolves in ~150ms; without the fix it hangs until the test's 10s abort timer fires. Fixes vercel#84504 Supersedes vercel#84525, vercel#92053
3 tasks
unstubbable
pushed a commit
to elishagreenwald/next.js
that referenced
this pull request
May 12, 2026
Avoid infinite peer-forward loops when middleware rewrites paths or the request was already forwarded (x-action-forwarded). The receiving worker short-circuits the forwarding decision and falls through to lenient serverModuleMap resolution instead of forwarding again. Adds a self-contained e2e regression test that reproduces the loop via a proxy.ts rewrite: with the fix the request resolves in ~150ms; without the fix it hangs until the test's 10s abort timer fires. Fixes vercel#84504 Supersedes vercel#84525, vercel#92053
unstubbable
added a commit
that referenced
this pull request
May 18, 2026
Reopens #93710 from a branch on this repo so the required deploy test matrix can run — GitHub Actions doesn't expose secrets to fork PRs, and those deploy jobs are required checks. All commits and credit carry forward from the original PR by @elishagreenwald, which built on earlier attempts by @LiamBoz in #84525 and @claygeo in #92053. When middleware rewrites a Server Action POST to a page that doesn't bundle the action, the receiving worker forwards the request to a worker that does. The forwarded request hits middleware again and gets rewritten the same way, so the new receiving worker forwards again — looping until undici's headers timeout (~300s) or memory pressure brings the server down (#84504). The fix is two related changes on the forwarding code path. The first is an `!actionWasForwarded` guard in `action-handler.ts` so a request carrying `x-action-forwarded: 1` is never forwarded a second time. The second is a new branch in `createForwardedActionResponse` that copies the `x-nextjs-action-not-found` header and 404 status onto the originating response when the forwarded worker can't find the action either; without it the client would see a generic "unexpected response" error instead of the `UnrecognizedActionError` that the rest of the framework expects. The e2e test at `test/e2e/app-dir/action-forward-loop` reproduces the scenario. A `proxy.ts` rewrites POSTs to `/with-action` so GETs still render the page normally; the page renders a `<form>` with an inline server action wrapped in a React error boundary that branches on `unstable_isUnrecognizedActionError`. The test clicks the button and waits for `#action-not-found-error`. Without the loop guard the test times out at 10s, and without the pass-through the boundary catches a generic error and the assertion fails — so both changes are individually load-bearing. This is a short-term fix. The mid-term direction is to remove server-side action forwarding altogether and have the client dispatch each Server Action to the route that actually bundles it, which makes the entire forwarding code path (and this bug surface) unnecessary. #90549 is the draft exploring that approach. Fixes #84504 Closes #93710 Closes #84525 Closes #92053 --------- Co-Authored-By: Elisha Greenwald <ejgreenwald@gmail.com> Co-Authored-By: LiamBoz <thisamazingnow@gmail.com> Co-Authored-By: kmaclip <kmartclips@proton.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
Fixes #84504
Add a minimal loop-detection guard to
createForwardedActionResponseso a Server Action request that has already been forwarded (marked byx-action-forwarded=1) is not forwarded again. Instead, we return early (no second forward).packages/next/src/server/app-render/action-handler.tsx-action-forwarded=1is present on the incoming request, return fromcreateForwardedActionResponsewithout forwarding again.Why?
In apps that use Middleware rewrites, a forwarded Server Action request can be rewritten to a path that triggers forwarding again, causing an infinite loop (e.g.
/rewrite/other → /rewrite/... → /rewrite/rewrite/... → …). This leads to repeated logs and a server action that never resolves.This guard breaks the cycle with very low risk by only affecting requests that were already forwarded once.
npm i && npm run devhttp://localhost:3000How?
At the start of
createForwardedActionResponse, inspect the incoming headers:x-action-forwarded=1is present → return (do not forward again).x-action-forwarded=1on the outgoing forwarded request.Works in both Node and Edge:
req.headersis already aHeaders(Edge), use it.HeadersAdapter.Test Plan
Manual (using the public repro)
/rewrite/...requests or log spam.Notes: This PR intentionally focuses on loop prevention. A broader follow-up could make forwarded requests mirror the pre-rewrite target to ensure resolution semantics remain identical under rewrites. That would be a separate change.
Risk
Low:
x-action-forwarded=1.For Contributors Checklist
Fixes #84504)pnpm prettier-fixlocally