Skip to content

fix: prevent infinite request loop when forwarding server actions with middleware rewrites#92053

Closed
claygeo wants to merge 2 commits into
vercel:canaryfrom
claygeo:fix/action-forward-infinite-loop
Closed

fix: prevent infinite request loop when forwarding server actions with middleware rewrites#92053
claygeo wants to merge 2 commits into
vercel:canaryfrom
claygeo:fix/action-forward-infinite-loop

Conversation

@claygeo

@claygeo claygeo commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Problem

Fixes #84504

When a server action fires right before navigation, Next.js forwards the action to the correct worker via createForwardedActionResponse. If middleware applies a rewrite to the forwarded request, the rewritten URL arrives at a worker that also wants to forward, creating an infinite request loop that never resolves.

Root Cause

In action-handler.ts, the forwarding guard at line 713 checks if (actionId) but does not check whether the action was already forwarded:

const actionWasForwarded = Boolean(req.headers['x-action-forwarded'])  // line 711: READ but not used as guard

if (actionId) {  // line 713: missing !actionWasForwarded check
  const forwardedWorker = selectWorkerForForwarding(actionId, page)
  if (forwardedWorker) {
    return createForwardedActionResponse(...)  // forwards again → infinite loop
  }
}

The x-action-forwarded header is already set on outgoing forwarded requests (createForwardedActionResponse, line 221) and read on incoming requests (line 711). The loop detection mechanism exists, it's just not wired into the forwarding guard.

Fix

Add !actionWasForwarded to the forwarding condition:

if (actionId && !actionWasForwarded) {

If an action has already been forwarded once (header present), skip the forwarding logic entirely. The action will be handled by the current worker instead of bouncing infinitely between workers.

5 lines changed in 1 file.

Note on PR #84525

PR #84525 addresses the same issue but has been abandoned for 6 months with zero reviews. This PR takes the same conceptual approach (check x-action-forwarded header) with a cleaner implementation at the caller level rather than inside createForwardedActionResponse.

claygeo added 2 commits March 28, 2026 13:13
When a malformed request body is sent to a server action endpoint
(e.g. by vulnerability scanners probing for CVE-2025-55182),
decodeReply throws a SyntaxError that bubbles up to the generic
catch handler and returns HTTP 500. This is incorrect — a malformed
client request should return 400 Bad Request, not 500 Internal
Server Error.

Wrap the decodeReply calls in both the edge/web runtime and node
runtime text body paths with try/catch for SyntaxError. Only
SyntaxError is caught and mapped to 400; all other errors are
re-thrown to preserve existing error handling behavior.

Fixes vercel#86945
…h middleware rewrites

When a server action fires during navigation, Next.js forwards the
action to the correct worker. If middleware applies a rewrite to the
forwarded request, the rewritten request arrives at a worker that also
tries to forward, creating an infinite loop.

The x-action-forwarded header is already set on forwarded requests
(createForwardedActionResponse, line 221) and read on incoming requests
(line 711), but the forwarding guard at line 713 only checks actionId
without checking whether the action was already forwarded. Add the
!actionWasForwarded condition to prevent re-forwarding.

Fixes vercel#84504
@nextjs-bot

Copy link
Copy Markdown
Contributor

Allow CI Workflow Run

  • approve CI run for commit: dfbf3c4

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
@elishagreenwald

Copy link
Copy Markdown
Contributor

Opened #93710 with the same fix + a deterministic e2e regression test

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
pull Bot pushed a commit to zys-contrib/next.js that referenced this pull request May 14, 2026
)

Reopens vercel#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 vercel#84525 and @claygeo in vercel#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 (vercel#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.
vercel#90549 is the draft exploring that approach.

Fixes vercel#84504

Closes vercel#93710
Closes vercel#84525
Closes vercel#92053

---------

Co-Authored-By: Elisha Greenwald <ejgreenwald@gmail.com>
Co-Authored-By: LiamBoz <thisamazingnow@gmail.com>
Co-Authored-By: kmaclip <kmartclips@proton.me>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Forwarded server actions with middleware rewrites cause an infinite request loop

3 participants