Skip to content

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

Merged
james-elicx merged 2 commits into
mainfrom
fix/issue-1340-no-server-actions-mpa
Jun 8, 2026
Merged

fix(actions): return 404 for MPA action on app with no server actions (#1340)#1853
james-elicx merged 2 commits into
mainfrom
fix/issue-1340-no-server-actions-mpa

Conversation

@james-elicx

Copy link
Copy Markdown
Member

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

Problem

When an app has no server actions and a non-fetch MPA / no-JS form POST (multipart/form-data) is submitted to a page, vinext rendered the page with 200 instead of Next.js' 404 + x-nextjs-action-not-found: 1. The fetch-action variant (via the Next-Action header) already returned 404; only the MPA/form-POST variant on a no-server-actions app was wrong.

Root cause: 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, React's decodeAction returns null (it does not throw), so the progressive handler fell through to a normal page render.

Fix

In the progressive (no-JS) action path, when decodeAction yields no action and the posted-to route is a page, return the Next.js action-not-found response (404 + x-nextjs-action-not-found: 1) and log Failed to find Server Action. This request might be from an older or newer deployment., mirroring Next.js.

The 404 is gated on the route being a page: route handlers (route.ts) are dispatched after action handling in vinext 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.

Tests

Ported from upstream test/e2e/app-dir/no-server-actions/no-server-actions.test.ts ("should error when triggering an MPA action on an app with no server actions") into tests/app-server-action-execution.test.ts:

  • 404 + action-not-found header + log when an MPA action targets a page with no server actions (fails without the fix).
  • Regression guard: multipart POSTs that decode to no action on a non-page route still fall through.

Closes #1340

…#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).
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@1853
npm i https://pkg.pr.new/vinext@1853

commit: b62af5d

@github-actions

github-actions Bot commented Jun 8, 2026

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
workers-cache preview production
web preview production

@ask-bonk

ask-bonk Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

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

I reviewed the diff, traced the surrounding action-dispatch code, confirmed parity against the upstream Next.js test, and ran the relevant suites. This is a solid, well-targeted fix. No blocking issues.

Verification performed

  • tests/app-server-action-execution.test.ts — 60 passed (including the 2 new cases)
  • tests/app-rsc-handler.test.ts + tests/app-router-dev-server.test.ts — 198 passed
  • vp check on all 3 changed files — clean (format, lint, types)
  • Compared against upstream test/e2e/app-dir/no-server-actions/no-server-actions.test.ts: behavior matches exactly (404 + x-nextjs-action-not-found: 1 + no-action-id log message for an MPA POST to a page).

What's correct

  • Right code path. The new branch fires only when decodeAction returns a non-function (!isAppServerActionFunction(action)) without throwing — the exact React decodeAction -> null shape for an MPA POST with no action. The pre-existing decode-miss tests (lines 729/760/776) all go through the separate catch path (isServerActionNotFoundError) and are unaffected, regardless of hasPageRoute.
  • hasPageRoute gating is sound. route.__loadPage && !route.__loadRouteHandler is a valid pure-page discriminator: per app-rsc-manifest.ts:269-271, __loadPage/__loadRouteHandler are set to lazy-loader thunks (truthy) only when pagePath/routePath exist, else null. They're static at module load, so reading them before lazy hydration is safe (correctly noted in the comment).
  • No regression for valid actions. A real action makes decodeAction return a function, so execution proceeds past the new branch; null only occurs when there genuinely is no action.
  • Layering follows AGENTS.md. The entry template only computes a 4-line boolean and delegates; the actual 404 decision lives in the typed server/ helper. matchRoute(cleanPathname) mirrors the adjacent handleServerActionRequest (app-rsc-entry.ts:828), so consistency is established.
  • createActionNotFoundResponse(null, ...) produces the no-action-id log line, matching upstream's MPA assertion. Cookie clearing / context cleanup match the other not-found exits.

Non-blocking observations

  1. No integration-level coverage for the new wiring. The MPA fix is unit-tested by passing hasPageRoute directly, but nothing exercises the entry-side matchRoute + __loadPage/__loadRouteHandler classification end-to-end. The existing dev-server tests (app-router-dev-server.test.ts:2004-2034) cover only the fetch-action variant (x-rsc-action header). Per AGENTS.md this unit-first approach is acceptable, but a dev-server test that POSTs multipart/form-data to a page (asserting 404) and to a route.ts (asserting fall-through) would lock in the marker-based gating that is currently untested. Consider as a follow-up.

  2. matchRoute now runs on every App Router request. handleProgressiveActionRequest is invoked for all requests (app-rsc-handler.ts:599), so the new matchRoute(cleanPathname) executes even for GETs / non-multipart POSTs that immediately fall through via isProgressiveServerActionRequest. Cost is negligible — app-rsc-handler.ts:590 already called matchRoute(cleanPathname) one line earlier (preActionMatch) — but it is a redundant second match. Minor; not worth blocking.

Conclusion

Behavior matches Next.js, tests are well-chosen and ported from upstream, the route-handler fall-through guard is a correct vinext-specific addition, and the layering is clean. LGTM.

@ask-bonk

ask-bonk Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #1853.

Summary

I reviewed PR #1853 (fix: return 404 for MPA action on app with no server actions, #1340). The fix is solid and well-targeted — LGTM with no blocking issues.

What I verified:

  • Ran tests/app-server-action-execution.test.ts (60 pass), tests/app-rsc-handler.test.ts + tests/app-router-dev-server.test.ts (198 pass), and vp check on all 3 changed files (clean).
  • Compared behavior against the upstream Next.js test (no-server-actions.test.ts) — matches exactly: 404 + x-nextjs-action-not-found: 1 + no-action-id log for an MPA POST to a page with no server actions.

Key correctness findings:

  • The new branch fires only on decodeAction returning null (not throwing), which is the precise MPA-no-action case; the pre-existing decode-miss tests use the separate throw/catch path and are unaffected.
  • hasPageRoute gating (__loadPage && !__loadRouteHandler) is a valid pure-page discriminator confirmed against the manifest generator; the markers are static and safe to read before lazy hydration.
  • The route-handler fall-through guard is a correct vinext-specific addition (route handlers dispatch after action handling).
  • Layering follows AGENTS.md — entry stays thin, decision lives in the typed server helper.

Two non-blocking observations:

  1. The entry-side wiring (matchRoute + marker classification) isn't covered end-to-end; only unit-tested by passing hasPageRoute directly. A dev-server integration test would lock it in — suggested as follow-up.
  2. matchRoute now runs redundantly on every App Router request (one line after an existing matchRoute call), but cost is negligible.

github run

…te 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.
@james-elicx

Copy link
Copy Markdown
Member Author

Thanks for the review. Addressed both non-blocking observations in b62af5d:

  1. Integration coverage for the entry-side wiring — added a dev-server test (tests/app-router-dev-server.test.ts) that POSTs multipart/form-data to /about (a page) and asserts 404 + x-nextjs-action-not-found: 1, exercising the entry's matchRoute + __loadPage/__loadRouteHandler classification end-to-end (not just hasPageRoute passed directly).

  2. Redundant matchRoute on every request — gated the progressive route match on isProgressiveServerActionRequest(request, contentType, actionId), so matchRoute now runs only for genuine multipart/no-actionId POST candidates instead of on every App Router request.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Re-review (post b62af5d): fix(actions): 404 for MPA action on app with no server actions (#1340)

I independently re-reviewed the diff after the follow-up commit, traced the dispatch path through app-rsc-handler.ts → generated entry → app-server-action-execution.ts, confirmed parity against upstream Next.js, and ran the relevant suites. No blocking issues — LGTM.

Verification performed

  • tests/app-server-action-execution.test.ts — 60 passed (incl. the 2 new cases)
  • tests/app-router-dev-server.test.ts -t "MPA form POST" — passed (POST /about → 404 + x-nextjs-action-not-found: 1 + Failed to find Server Action… log)
  • tests/app-rsc-handler.test.ts — 35 passed
  • vp check on all 4 changed files — clean (format, lint, types)
  • Diffed against upstream test/e2e/app-dir/no-server-actions/no-server-actions.test.ts: behavior matches the MPA assertion exactly (404 + header + no-action-id log line).

Correctness findings

  • Right trigger. The new 404 fires only when decodeAction returns a non-function (the React decodeAction → null shape for a multipart POST with no encoded action), which is mutually exclusive with the throw-based decode-miss handled by the catch at line 879. The pre-existing decode-miss tests (throw path, isServerActionNotFoundError) are unaffected regardless of hasPageRoute. Confirmed the integration test triggers null even in the app-basic fixture (which has actions) because the form carries no `` field — matching Next.js' "any multipart POST to a page is an action attempt" model.
  • hasPageRoute gate is sound. Per app-rsc-manifest.ts:254-257, __loadPage/__loadRouteHandler are truthy lazy-loader thunks only when pagePath/routePath exist (else "null"). __loadPage && !__loadRouteHandler is a valid pure-page discriminator; markers are static at module load, so reading them before lazy hydration is safe.
  • Route-handler fall-through preserved. route.ts POSTs (dispatched after action handling in vinext) keep falling through — covered by the new hasPageRoute: false unit test.
  • Valid actions unaffected. A real action makes decodeAction return a function, skipping the new branch; the form-state re-render path (lines 815-848) is untouched.
  • Dev/prod parity. This dispatch lives in the single generated RSC entry; App Router prod delegates to the built entry, so it inherits the fix. Pages Router has no server-action dispatch, so no parallel change is needed. Confirmed only one call site exists.
  • Follow-up commit addressed prior feedback well. matchRoute is now gated behind isProgressiveServerActionRequest(...), so it no longer runs on every App Router request — only on genuine multipart/no-actionId POST candidates. The added dev-server test exercises the entry-side matchRoute + marker classification end-to-end rather than passing hasPageRoute directly.

Non-blocking observations

  1. Second match for genuine progressive POSTs. app-rsc-handler.ts:590 already computes preActionMatch = matchRoute(cleanPathname) against the identical cleanPathname passed into the handler. The progressive path re-matches instead of reusing preActionMatch. Cost is negligible and it mirrors the adjacent handleServerActionRequest pattern (line 838), but threading preActionMatch through the handler options would eliminate the duplicate match entirely. Optional cleanup.

Conclusion

Behavior matches Next.js, the page-vs-route gating is correct and now covered both at the unit and integration level, layering follows AGENTS.md (entry stays thin; decision lives in the typed server helper), and there's no regression for valid actions or route handlers. LGTM.

@ask-bonk

ask-bonk Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #1853.

Summary

I re-reviewed PR #1853 (fix: 404 for MPA action on app with no server actions, #1340) including the follow-up commit b62af5d. LGTM — no blocking issues.

Verified:

  • tests/app-server-action-execution.test.ts (60 pass), the new app-router-dev-server.test.ts MPA case (POST /about → 404 + x-nextjs-action-not-found: 1 + log), tests/app-rsc-handler.test.ts (35 pass), and vp check on all 4 files (clean).
  • Diffed against upstream no-server-actions.test.ts — behavior matches exactly.

Key correctness findings:

  • The new 404 fires only when decodeAction returns a non-function (mutually exclusive with the throw-based catch path), so pre-existing decode-miss tests are unaffected.
  • Confirmed the integration test correctly triggers decodeAction → null even in the app-basic fixture (which has actions) because the form carries no action reference — matching Next.js' "any multipart POST to a page is an action attempt" model.
  • hasPageRoute gating (__loadPage && !__loadRouteHandler) verified against the manifest generator as a valid pure-page discriminator; markers are static and safe to read pre-hydration.
  • Route-handler fall-through preserved; valid actions unaffected; single dispatch site means dev/prod parity holds.
  • Follow-up commit correctly gated matchRoute behind isProgressiveServerActionRequest and added end-to-end integration coverage.

One non-blocking observation: app-rsc-handler.ts:590 already computes preActionMatch against the identical cleanPathname; the progressive path re-matches rather than reusing it. Negligible cost, optional cleanup.

github run

@james-elicx
james-elicx marked this pull request as ready for review June 8, 2026 20:26
@james-elicx
james-elicx merged commit 76a39cb into main Jun 8, 2026
51 checks passed
@james-elicx
james-elicx deleted the fix/issue-1340-no-server-actions-mpa branch June 8, 2026 20:30
@github-actions github-actions Bot mentioned this pull request Jun 8, 2026
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.

Server actions: incorrect error handling (500 instead of 404, error boundary propagation)

1 participant