[PR #985] [MERGED] fix(app-router): return action-not-found for stale server actions #1002

Closed
opened 2026-05-06 13:11:30 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/985
Author: @NathanDrake2406
Created: 4/30/2026
Status: Merged
Merged: 4/30/2026
Merged by: @james-elicx

Base: mainHead: nathan/server-action-not-found


📝 Commits (2)

  • 7bf5d4a fix(app-router): return action-not-found for stale server actions
  • 8d5c6ec chore: rerun ci

📊 Changes

5 files changed (+240 additions, -10 deletions)

View changed files

📝 packages/vinext/src/server/app-browser-entry.ts (+8 -0)
📝 packages/vinext/src/server/app-server-action-execution.ts (+51 -2)
packages/vinext/src/server/server-action-not-found.ts (+58 -0)
📝 tests/app-router.test.ts (+7 -7)
📝 tests/app-server-action-execution.test.ts (+116 -1)

📄 Description

What this changes

Vinext now returns the Next.js action-not-found protocol for unknown or stale App Router Server Action ids:

  • 404 response status
  • x-nextjs-action-not-found: 1
  • Server action not found. response body
  • client-side action callback throws the matching not-found error before attempting to decode a Flight payload

Why

Stale Server Action ids can happen during deployment skew or from an older client tab. Before this change, loadServerAction() failures fell through the generic server action error reporter and returned 500, which made expected skew look like an application failure and skipped the client recovery signal.

Next.js treats this as a dedicated not-found protocol:

Approach

The action-not-found behavior is implemented in a normal runtime module, server-action-not-found.ts, and the generated/runtime entry paths only wire through that behavior. This keeps with the project rule that codegen describes app shape while normal modules implement behavior.

The RSC action helper now treats loadServerAction() as an external runtime boundary returning unknown, narrows it before invocation, and maps only recognized missing-action cases to the action-not-found response. Unrelated loader failures still go through the existing generic 500 reporter path.

Validation

  • vp test run tests/app-server-action-execution.test.ts
  • vp test run tests/app-router.test.ts -t "allows server action POST"
  • vp test run tests/app-server-action-execution.test.ts tests/app-router.test.ts -t "allows server action POST|stale fetch action ids|progressive action decode misses|server action export is missing|unrelated server action loader failures"
  • vp run vinext#build
  • vp check

Note: in this fresh worktree, running vp check before vp run vinext#build failed because benchmarks/vinext/vite.config.ts could not resolve the workspace vinext package. After the package build created the local dist artifacts, vp check passed cleanly.

Risks / follow-ups

The missing-action classifier is intentionally narrow: it recognizes Next.js' failed-action messages and Vite RSC's invalid server reference message, while preserving generic loader failures as 500s. Malformed action payload validation still runs before action lookup in Vinext to preserve the existing payload-hardening behavior.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/985 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 4/30/2026 **Status:** ✅ Merged **Merged:** 4/30/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `nathan/server-action-not-found` --- ### 📝 Commits (2) - [`7bf5d4a`](https://github.com/cloudflare/vinext/commit/7bf5d4a98e9392d935a99bdfcd62a3c23885385c) fix(app-router): return action-not-found for stale server actions - [`8d5c6ec`](https://github.com/cloudflare/vinext/commit/8d5c6eccb4216b4f38752c64394cd69e2f49d157) chore: rerun ci ### 📊 Changes **5 files changed** (+240 additions, -10 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/server/app-browser-entry.ts` (+8 -0) 📝 `packages/vinext/src/server/app-server-action-execution.ts` (+51 -2) ➕ `packages/vinext/src/server/server-action-not-found.ts` (+58 -0) 📝 `tests/app-router.test.ts` (+7 -7) 📝 `tests/app-server-action-execution.test.ts` (+116 -1) </details> ### 📄 Description ## What this changes Vinext now returns the Next.js action-not-found protocol for unknown or stale App Router Server Action ids: - `404` response status - `x-nextjs-action-not-found: 1` - `Server action not found.` response body - client-side action callback throws the matching not-found error before attempting to decode a Flight payload ## Why Stale Server Action ids can happen during deployment skew or from an older client tab. Before this change, `loadServerAction()` failures fell through the generic server action error reporter and returned `500`, which made expected skew look like an application failure and skipped the client recovery signal. Next.js treats this as a dedicated not-found protocol: - Server response header/status: [`handleUnrecognizedFetchAction`](https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/action-handler.ts#L562-L577) - Fetch action id validation before decoding args: [`getActionModIdOrError` call](https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/action-handler.ts#L764-L768) and [`getActionModIdOrError`](https://github.com/vercel/next.js/blob/canary/packages/next/src/server/app-render/action-handler.ts#L1348-L1369) - Client interpretation of `x-nextjs-action-not-found`: [`server-action-reducer.ts`](https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts#L173-L178) - Observable e2e contract: [`no-server-actions.test.ts`](https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/no-server-actions/no-server-actions.test.ts#L8-L47) ## Approach The action-not-found behavior is implemented in a normal runtime module, `server-action-not-found.ts`, and the generated/runtime entry paths only wire through that behavior. This keeps with the project rule that codegen describes app shape while normal modules implement behavior. The RSC action helper now treats `loadServerAction()` as an external runtime boundary returning `unknown`, narrows it before invocation, and maps only recognized missing-action cases to the action-not-found response. Unrelated loader failures still go through the existing generic 500 reporter path. ## Validation - `vp test run tests/app-server-action-execution.test.ts` - `vp test run tests/app-router.test.ts -t "allows server action POST"` - `vp test run tests/app-server-action-execution.test.ts tests/app-router.test.ts -t "allows server action POST|stale fetch action ids|progressive action decode misses|server action export is missing|unrelated server action loader failures"` - `vp run vinext#build` - `vp check` Note: in this fresh worktree, running `vp check` before `vp run vinext#build` failed because `benchmarks/vinext/vite.config.ts` could not resolve the workspace `vinext` package. After the package build created the local dist artifacts, `vp check` passed cleanly. ## Risks / follow-ups The missing-action classifier is intentionally narrow: it recognizes Next.js' failed-action messages and Vite RSC's invalid server reference message, while preserving generic loader failures as 500s. Malformed action payload validation still runs before action lookup in Vinext to preserve the existing payload-hardening behavior. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:11:30 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#1002
No description provided.