Skip to content

fix(app-router): handle Vite ref errors that elide #exportName (#1340) - #1668

Merged
james-elicx merged 1 commit into
mainfrom
fix/issue-1340-action-not-found-status
May 28, 2026
Merged

fix(app-router): handle Vite ref errors that elide #exportName (#1340)#1668
james-elicx merged 1 commit into
mainfrom
fix/issue-1340-action-not-found-status

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • Vite's reference-validation virtual module emits [vite-rsc] invalid server reference '<moduleId>' without the #<exportName> suffix, because loadServerAction(id) splits the id at # before calling requireModule(file). isServerActionNotFoundError only compared the full action id against the Vite error, so stale/missing references surfaced as a generic 500 instead of Next.js' 404 + x-nextjs-action-not-found response.
  • Accept both the full-actionId and pre-# module-path forms for the dev ([vite-rsc] invalid server reference '<id>') and prod (server reference not found '<id>') error shapes.

Scoped fix for issue #1340 — the notFound()-from-server-action 404 status path. Other parts of #1340 (error-boundary propagation for text/plain action responses, unrecognized-action client behaviour) remain in scope for follow-up PRs.

Test plan

  • pnpm test tests/app-server-action-execution.test.ts — new unit test fails on main, passes after fix
  • pnpm test tests/nextjs-compat/not-found.test.ts — integration coverage that notFound() from a server action returns 404 for both fetch and progressive form action paths
  • pnpm run check (format / lint / type) clean
  • CI: Check, Vitest, Playwright E2E

#1340)

When a server action id resolved from request headers carries the
`#<exportName>` suffix (e.g. `/app/foo.ts#bar`), `loadServerAction()`
splits the id at `#` and calls `requireModule(file)` with the module
path only. `@vitejs/plugin-rsc`'s reference-validation virtual module
therefore emits `[vite-rsc] invalid server reference '/app/foo.ts'`,
omitting the `#<exportName>` portion. The previous match in
`isServerActionNotFoundError` compared the full actionId against the
Vite error string, so missing/stale references surfaced as a generic
500 instead of the Next.js-compatible 404 with the
`x-nextjs-action-not-found` header.

Also accept the pre-`#` module-path form for both the dev-time
`[vite-rsc] invalid server reference '<id>'` and prod-time
`server reference not found '<id>'` error shapes.

Adds a focused unit test reproducing the dev-mode case and integration
tests asserting that throwing `notFound()` from a server action returns
404 (both fetch and progressive form paths), porting fixture from
Next.js: test/e2e/app-dir/actions/app/server/form.js.
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
web preview production

@ask-bonk ask-bonk Bot left a comment

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.

Clean, well-scoped fix. The root cause analysis is solid — loadServerAction(id) splits at # before requireModule, so the Vite reference-validator only sees the module path, not the full module#export action id. The existing isServerActionNotFoundError was comparing against the full action id and never matched, causing the error to fall through to the generic 500 path instead of the 404 + x-nextjs-action-not-found response.

Implementation review:

The moduleId extraction via actionId.split("#")[0] with the moduleId !== actionId guard is the right approach — when the action id has no # (e.g. "stale-action-id"), moduleId === actionId so the second branch is skipped, preserving the existing behavior for those cases. The duplication across the dev/prod patterns is a reasonable trade-off for readability given there are only two patterns.

Test coverage:

  • Unit test directly exercises the elided-#exportName error shape — good regression anchor.
  • The revalidating-fetch-action 404 status test ("preserves the 404 status when notFound() is thrown by a revalidating fetch action") covers the actionStatus propagation through the page-rerender branch (line 818 of app-server-action-execution.ts), which was previously only tested for the skip-rerender shortcut. This is a separate but related concern and the test is well-structured.
  • Integration tests for both fetch-action and progressive-form-action paths against the live dev server are thorough. The pre-visit to /actions to ensure the "use server" module is registered is a necessary detail.

One minor nit (non-blocking): the notFoundAction fixture function is also used by the integration tests, which is correct. Just noting it's clean that both the unit and integration tests exercise different layers of the same fix.

Looks good to merge once CI is green.

@ask-bonk

ask-bonk Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Approved PR #1668. The fix correctly handles the mismatch between the full action id (module#export) passed by the caller and the module-path-only id (module) emitted by Vite's reference validator. The guard logic, test coverage across both unit and integration layers, and the scoped fixture addition are all clean. Ready to merge once CI passes.

github run

@pkg-pr-new

pkg-pr-new Bot commented May 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1668

commit: 8f8e70d

@james-elicx
james-elicx marked this pull request as ready for review May 28, 2026 16:44
@james-elicx
james-elicx merged commit f4687b3 into main May 28, 2026
38 checks passed
@james-elicx
james-elicx deleted the fix/issue-1340-action-not-found-status branch May 28, 2026 20:13
james-elicx added a commit that referenced this pull request Jun 8, 2026
…#1340) (#1853)

* fix(actions): return 404 for MPA action on app with no server actions (#1340)

A multipart form POST to an App Router page is always a server-action
attempt. When the form body decodes to no action at all (e.g. the build
has no server actions, so decodeAction returns null rather than
throwing), vinext fell through to a 200 page render instead of Next.js'
404 + x-nextjs-action-not-found. The fetch-action variant (via the
Next-Action header) already worked; only the MPA/no-JS form-POST variant
was wrong.

Gate the new 404 on the posted-to route being a page: route handlers run
after action dispatch and legitimately accept raw multipart POSTs, so
they still fall through. The page-vs-route distinction uses the static
__loadPage / __loadRouteHandler markers, available before lazy module
hydration.

Finishes the no-server-actions MPA case of #1340 (error-boundary,
content-type, and unrecognized-action handling landed in #1386/#1668).

* test(actions): add MPA no-action integration test; skip redundant route match (#1340)

Address ask-bonk review on #1853:
- Add a dev-server integration test that POSTs multipart/form-data to a
  page (/about) and asserts 404 + x-nextjs-action-not-found, locking in
  the entry-side matchRoute + __loadPage/__loadRouteHandler classification
  end-to-end (previously only unit-tested by passing hasPageRoute directly).
- Gate the progressive-action route match on isProgressiveServerActionRequest
  so matchRoute no longer runs on every App Router request — only for actual
  multipart/no-actionId POST candidates.
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.

1 participant