experiment with removing action forwarding logic#90549
Conversation
Failing test suitesCommit: d72e46c | About building and testing Next.js
Expand output● app-dir action handling › should dispatch delayed action request to a route that contains the action handler (node) ● app-dir action handling › should dispatch delayed action request to a route that contains the action handler (edge) ● app-dir action handling › should not error when a delayed action triggers a redirect (node) ● app-dir action handling › should not error when a delayed action triggers a redirect (edge)
Expand output● actions-tree-shaking - reexport › should keep all the action exports for namespace export case on client layer
Expand output● app-dir action handling › should dispatch delayed action request to a route that contains the action handler (node) ● app-dir action handling › should dispatch delayed action request to a route that contains the action handler (edge) ● app-dir action handling › should not error when a delayed action triggers a redirect (node) ● app-dir action handling › should not error when a delayed action triggers a redirect (edge)
Expand output● app dir - navigation › browser back to a revalidated page › should load the page
Expand output● dynamic-interception-route-revalidate › should refresh the dynamic intercepted route when the interception route is revalidated
Expand output● parallel-routes-revalidation › router.refresh (regular) - searchParams: false › should correctly refresh data for the intercepted route and previously active page slot ● parallel-routes-revalidation › router.refresh (regular) - searchParams: false › should correctly refresh data for previously intercepted modal and active page slot ● parallel-routes-revalidation › router.refresh (regular) - searchParams: true › should correctly refresh data for the intercepted route and previously active page slot ● parallel-routes-revalidation › router.refresh (regular) - searchParams: true › should correctly refresh data for previously intercepted modal and active page slot ● parallel-routes-revalidation › router.refresh (dynamic) - searchParams: false › should correctly refresh data for the intercepted route and previously active page slot ● parallel-routes-revalidation › router.refresh (dynamic) - searchParams: false › should correctly refresh data for previously intercepted modal and active page slot ● parallel-routes-revalidation › router.refresh (dynamic) - searchParams: true › should correctly refresh data for the intercepted route and previously active page slot ● parallel-routes-revalidation › router.refresh (dynamic) - searchParams: true › should correctly refresh data for previously intercepted modal and active page slot ● parallel-routes-revalidation › server action revalidation › handles refreshing when multiple parallel slots are active
Expand output● app-dir action handling › should handle actions correctly after navigation / redirection events ● app-dir action handling › should handle actions correctly after following a relative link
Expand output● app-dir action handling › should dispatch delayed action request to a route that contains the action handler (node) ● app-dir action handling › should dispatch delayed action request to a route that contains the action handler (edge) ● app-dir action handling › should not error when a delayed action triggers a redirect (node) ● app-dir action handling › should not error when a delayed action triggers a redirect (edge)
Expand output● avoid-popstate-flash › does not flash back to partial PPR data during back/forward navigation |
Stats from current PR🟢 1 improvement
📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles: **400 kB** → **400 kB**
|
| Canary | PR | Change | |
|---|---|---|---|
| middleware-b..fest.js gzip | 764 B | 765 B | ✓ |
| Total | 764 B | 765 B |
Build Details
Build Manifests
| Canary | PR | Change | |
|---|---|---|---|
| _buildManifest.js gzip | 451 B | 452 B | ✓ |
| Total | 451 B | 452 B |
📦 Webpack
Client
Main Bundles
| Canary | PR | Change | |
|---|---|---|---|
| 5528-HASH.js gzip | 5.54 kB | N/A | - |
| 6280-HASH.js gzip | 58.3 kB | N/A | - |
| 6335.HASH.js gzip | 169 B | N/A | - |
| 912-HASH.js gzip | 4.59 kB | N/A | - |
| e8aec2e4-HASH.js gzip | 62.6 kB | N/A | - |
| framework-HASH.js gzip | 59.7 kB | 59.7 kB | ✓ |
| main-app-HASH.js gzip | 255 B | 254 B | ✓ |
| main-HASH.js gzip | 39.1 kB | 39.1 kB | ✓ |
| webpack-HASH.js gzip | 1.68 kB | 1.68 kB | ✓ |
| 262-HASH.js gzip | N/A | 4.59 kB | - |
| 2889.HASH.js gzip | N/A | 169 B | - |
| 5602-HASH.js gzip | N/A | 5.55 kB | - |
| 6948ada0-HASH.js gzip | N/A | 62.6 kB | - |
| 9544-HASH.js gzip | N/A | 59.1 kB | - |
| Total | 232 kB | 233 kB |
Polyfills
| Canary | PR | Change | |
|---|---|---|---|
| polyfills-HASH.js gzip | 39.4 kB | 39.4 kB | ✓ |
| Total | 39.4 kB | 39.4 kB | ✓ |
Pages
| Canary | PR | Change | |
|---|---|---|---|
| _app-HASH.js gzip | 194 B | 194 B | ✓ |
| _error-HASH.js gzip | 183 B | 180 B | 🟢 3 B (-2%) |
| css-HASH.js gzip | 331 B | 330 B | ✓ |
| dynamic-HASH.js gzip | 1.81 kB | 1.81 kB | ✓ |
| edge-ssr-HASH.js gzip | 256 B | 256 B | ✓ |
| head-HASH.js gzip | 351 B | 352 B | ✓ |
| hooks-HASH.js gzip | 384 B | 383 B | ✓ |
| image-HASH.js gzip | 580 B | 581 B | ✓ |
| index-HASH.js gzip | 260 B | 260 B | ✓ |
| link-HASH.js gzip | 2.5 kB | 2.5 kB | ✓ |
| routerDirect..HASH.js gzip | 320 B | 319 B | ✓ |
| script-HASH.js gzip | 386 B | 386 B | ✓ |
| withRouter-HASH.js gzip | 315 B | 315 B | ✓ |
| 1afbb74e6ecf..834.css gzip | 106 B | 106 B | ✓ |
| Total | 7.97 kB | 7.97 kB | ✅ -2 B |
Server
Edge SSR
| Canary | PR | Change | |
|---|---|---|---|
| edge-ssr.js gzip | 125 kB | 125 kB | ✓ |
| page.js gzip | 254 kB | 254 kB | ✓ |
| Total | 379 kB | 379 kB |
Middleware
| Canary | PR | Change | |
|---|---|---|---|
| middleware-b..fest.js gzip | 615 B | 615 B | ✓ |
| middleware-r..fest.js gzip | 156 B | 155 B | ✓ |
| middleware.js gzip | 43.6 kB | 43.9 kB | ✓ |
| edge-runtime..pack.js gzip | 842 B | 842 B | ✓ |
| Total | 45.2 kB | 45.5 kB |
Build Details
Build Manifests
| Canary | PR | Change | |
|---|---|---|---|
| _buildManifest.js gzip | 715 B | 718 B | ✓ |
| Total | 715 B | 718 B |
Build Cache
| Canary | PR | Change | |
|---|---|---|---|
| 0.pack gzip | 4.01 MB | 4.02 MB | 🔴 +10.2 kB (+0%) |
| index.pack gzip | 103 kB | 103 kB | ✓ |
| index.pack.old gzip | 104 kB | 103 kB | 🟢 1.22 kB (-1%) |
| Total | 4.22 MB | 4.23 MB |
🔄 Shared (bundler-independent)
Runtimes
| Canary | PR | Change | |
|---|---|---|---|
| app-page-exp...dev.js gzip | 320 kB | 320 kB | ✓ |
| app-page-exp..prod.js gzip | 170 kB | 169 kB | ✓ |
| app-page-tur...dev.js gzip | 319 kB | 319 kB | ✓ |
| app-page-tur..prod.js gzip | 169 kB | 169 kB | ✓ |
| app-page-tur...dev.js gzip | 316 kB | 316 kB | ✓ |
| app-page-tur..prod.js gzip | 168 kB | 167 kB | ✓ |
| app-page.run...dev.js gzip | 316 kB | 316 kB | ✓ |
| app-page.run..prod.js gzip | 168 kB | 168 kB | ✓ |
| app-route-ex...dev.js gzip | 70.8 kB | 70.8 kB | ✓ |
| app-route-ex..prod.js gzip | 49.2 kB | 49.2 kB | ✓ |
| app-route-tu...dev.js gzip | 70.8 kB | 70.8 kB | ✓ |
| app-route-tu..prod.js gzip | 49.2 kB | 49.2 kB | ✓ |
| app-route-tu...dev.js gzip | 70.4 kB | 70.4 kB | ✓ |
| app-route-tu..prod.js gzip | 49 kB | 49 kB | ✓ |
| app-route.ru...dev.js gzip | 70.4 kB | 70.4 kB | ✓ |
| app-route.ru..prod.js gzip | 49 kB | 49 kB | ✓ |
| dist_client_...dev.js gzip | 324 B | 324 B | ✓ |
| dist_client_...dev.js gzip | 326 B | 326 B | ✓ |
| dist_client_...dev.js gzip | 318 B | 318 B | ✓ |
| dist_client_...dev.js gzip | 317 B | 317 B | ✓ |
| pages-api-tu...dev.js gzip | 43.2 kB | 43.2 kB | ✓ |
| pages-api-tu..prod.js gzip | 32.9 kB | 32.9 kB | ✓ |
| pages-api.ru...dev.js gzip | 43.2 kB | 43.2 kB | ✓ |
| pages-api.ru..prod.js gzip | 32.8 kB | 32.8 kB | ✓ |
| pages-turbo....dev.js gzip | 52.5 kB | 52.5 kB | ✓ |
| pages-turbo...prod.js gzip | 38.5 kB | 38.5 kB | ✓ |
| pages.runtim...dev.js gzip | 52.5 kB | 52.5 kB | ✓ |
| pages.runtim..prod.js gzip | 38.4 kB | 38.4 kB | ✓ |
| server.runti..prod.js gzip | 62 kB | 62 kB | ✓ |
| Total | 2.82 MB | 2.82 MB | ✅ -1.49 kB |
📝 Changed Files (8 files)
Files with changes:
app-page-exp..ntime.dev.jsapp-page-exp..time.prod.jsapp-page-tur..ntime.dev.jsapp-page-tur..time.prod.jsapp-page-tur..ntime.dev.jsapp-page-tur..time.prod.jsapp-page.runtime.dev.jsapp-page.runtime.prod.js
View diffs
app-page-exp..ntime.dev.js
failed to diffapp-page-exp..time.prod.js
failed to diffapp-page-tur..ntime.dev.js
failed to diffapp-page-tur..time.prod.js
failed to diffapp-page-tur..ntime.dev.js
failed to diffapp-page-tur..time.prod.js
failed to diffapp-page.runtime.dev.js
failed to diffapp-page.runtime.prod.js
failed to diff📎 Tarball URL
next@https://vercel-packages.vercel.app/next/prs/90549/next
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>
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>

No description provided.