fix(app-router): align server action routing - #2520
Conversation
|
/bigbonk review for issues |
commit: |
|
|
/bigbonk review for issues |
Performance benchmarksCompared 0 improved · 1 regressed · 5 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
Review: server action routing alignmentReviewed the full diff, ran the targeted suites ( A few observations, none blocking: 1. Action-owner injection is gated behind the classification stub filter (fragile coupling)In
The action-owner stub is only replaced if the chunk also contains The 2. Dev vs prod ownership divergence through node_modules
3.
|
|
Posted the review to PR #2520. Summary of what I found: Review outcomeThe PR is well-implemented and the security model is sound. I verified:
Issues raised (none blocking)
|
|
/bigbonk review for issues |
| ${ | ||
| hasServerActions | ||
| ? `const __loadAppServerActionExecution = () => import(${JSON.stringify(appServerActionExecutionPath)});` | ||
| ? `const __loadAppServerActionExecution = () => import(${JSON.stringify(appServerActionExecutionPath)}); |
| hasServerActions | ||
| ? `const __loadAppServerActionExecution = () => import(${JSON.stringify(appServerActionExecutionPath)});` | ||
| ? `const __loadAppServerActionExecution = () => import(${JSON.stringify(appServerActionExecutionPath)}); | ||
| const __loadAppActionForwarding = () => import(${JSON.stringify(appActionForwardingPath)});` |
|
/bigbonk review for issues |
Follow-up review — action-owner injection robustness + build costRe-reviewed the diff, re-ran
Two things worth addressing, only the first is more than cosmetic: 1. Reinforce the
|
|
Posted the review to PR #2520. Review summaryI re-reviewed the full diff, re-ran the targeted suites (
Issues raised (none blocking)
This complements the prior |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: server action routing alignment (follow-up)
Re-reviewed the current head (f698182, which is 4 commits past the first ask-bonk pass) and re-ran the targeted suites — tests/app-action-forwarding.test.ts, tests/action-owner-manifest.test.ts, and the production-build tests/action-runtime-security.test.ts all pass (42 tests green).
The security model still holds up and two prior review items are now resolved:
x-action-forwardedcannot be spoofed — it is inINTERNAL_HEADERS(headers.ts:211), sofilterInternalHeadersstrips any client copy (app-rsc-handler.ts:1316) and it is re-injected only via the trustedctx.actionForwardedpath (app-rsc-handler.ts:1317-1319). The re-entry loop guard fails closed to{}(app-action-forwarding.ts:149-152).- Prior review #1 (fragile
__VINEXT_CLASScoupling) is fixed.renderChunknow gates the action-owner injection on its own independenthasActionOwnerStubcondition (index.ts:3664-3666) rather than piggybacking the classification pre-filter, andgenerateBundle(index.ts:3744-3750) now fails the build loudly if__VINEXT_ACTION_OWNERS_STUB__survives un-injected. This converts the prior silent-runtime-404 risk into a hard build error. - Prior review #5 (cookie deletion untested) is fixed —
tests/app-action-forwarding.test.ts:133-183now coversMax-Age=0,Max-Age=-1, and expired-Expiresdeletions.
1. CodeQL "improper code sanitization" — the manifest is embedded into generated JS with raw JSON.stringify (should use safeJsonStringify)
The two CodeQL alerts on app-rsc-entry.ts are legitimate and both trace back to embedding the action-owner manifest into generated JavaScript source without HTML/JS-safe escaping:
- Dev path:
app-rsc-entry.ts:708—function __VINEXT_ACTION_OWNERS() { return ${JSON.stringify(actionOwners)}; } - Prod path:
injectActionOwnerManifestataction-owner-manifest.ts:375-378—`function ${match[1]}() { return ${JSON.stringify(manifest)}; }`
The manifest keys and values are developer-controlled strings, but they are not constants: in dev, keys are filesystem-derived reference paths (referenceKey → /@fs/${id} or /${relative}, action-owner-manifest.ts:90-96) and values are route patterns derived from directory names. JSON.stringify does not escape <, >, &, U+2028, or U+2029 — exactly the characters that break JS/HTML embedding. This repo already has the correct helper for this: safeJsonStringify in server/html.ts, which is used everywhere else data is serialized into emitted JS (app-ssr-stream.ts, dev-server.ts, pages-server-entry.ts, etc.). The classification injector avoided the alert only because it emits an integer switch dispatch, not embedded strings.
Recommend swapping both sites to safeJsonStringify. It produces valid JSON that is also a valid JS literal, so runtime JSON.parse/object-literal semantics are unchanged, and it clears the CodeQL alerts. generateRscEntry can import it the same way the other entry templates do; injectActionOwnerManifest is in build/ and can import from ../server/html.js.
2. Forwarded dispatch drops the query string (forwardUrl.search = "")
forwardServerActionIfNeeded clears the search params on the forward (app-action-forwarding.ts:157). For the action invocation itself this is fine (the action is keyed by header, not query), but the owner page re-render triggered by the forward loses the original request's query string. If an action's owner page reads searchParams during the post-action render, it will see an empty set on the forwarded path but the real params on the non-forwarded (same-route) path — a dev/prod-adjacent behavior split worth a comment or a test. Worth confirming against Next.js' forwarded-action behavior before merge; if intentional, a one-line comment explaining why the query is dropped would prevent a future "bug fix" that re-adds it.
3. (Carry-over, non-blocking) Eager static manifest cost on every prod build
buildStaticActionOwnerManifest still runs unconditionally in the load hook (index.ts:3509-3519) and AST-parses every route-reachable module, even though in production the authoritative source is the Rollup module graph and staticOwners is only consulted for the node_modules re-export edge case (action-owner-manifest.ts:357). This lines up with the +11.8% production-build-time regression the benchmark bot flagged. Narrowing the static pass to only node_modules re-export chains (or gating it on their presence) would keep the common case off that cost. Tracked from the prior review; not a merge blocker.
Nice work on the injection-guard hardening and the thin-entry delegation. Only #1 is more than cosmetic, and it has a clean mechanical fix.
|
Review posted to PR #2520. SummaryI reviewed the current head ( Security model confirmed sound, and two prior review items are now resolved:
Issues raised (only #1 is more than cosmetic):
|
|
/bigbonk review for issues |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: server action routing alignment (head 2dcf18a, 5 commits)
Re-reviewed the current head, which is one commit (2dcf18a "escape action owner metadata") past the prior ask-bonk pass. Re-ran the targeted suites and vp check:
tests/app-action-forwarding.test.ts+tests/action-owner-manifest.test.ts— 34 passtests/action-runtime-security.test.ts+tests/app-server-action-execution.test.ts+tests/entry-templates.test.ts— 140 passtests/app-browser-server-action-client.test.ts— 3 passvp checkon the changed source — clean
All substantive items from prior reviews are now resolved. The latest commit and the earlier hardening close the loop:
-
Prior #1 (fragile
__VINEXT_CLASScoupling) — fixed.renderChunknow gates action-owner injection on its own independenthasActionOwnerStubcondition (index.ts:3664-3666), andgenerateBundle(index.ts:3744-3750) fails the build loudly if__VINEXT_ACTION_OWNERS_STUB__survives un-injected. Converts the silent-runtime-404 risk into a hard build error. -
CodeQL "improper code sanitization" — fixed. Both injection sites now use
safeJsonStringifyinstead of rawJSON.stringify: the dev path (app-rsc-entry.ts:709) and the prod injector (action-owner-manifest.ts:378).safeJsonStringifyescapes<,>,&, U+2028, U+2029 (server/html.ts:21). Covered by a new test (action-owner-manifest.test.ts"escapes action owner manifests embedded in generated JavaScript"). -
Prior #5 (cookie deletion untested) — fixed. Tests now cover
Max-Age=0,Max-Age=-1, and expiredExpires(app-action-forwarding.test.ts:133-183). -
Prior #3 (ExecutionContext not forwarded) — now addressed. The forward dispatch threads the real CF context through (
app-rsc-entry.ts:1064-1076): when__getRequestExecutionContext()resolves, it passeswaitUntil/cache/passThroughOnExceptionalongsideactionForwarded: true, soisExecutionContextLike(app-rsc-handler.ts:349) recognizes it andafter()/waitUntilinside a forwarded action attach to the request lifetime. -
Prior #2 (dropped query string) — verified against Next.js and now correct + documented.
forwardServerActionIfNeededclearsforwardUrl.search(app-action-forwarding.ts:159), matching Next.jscreateForwardedActionResponse()which buildsnew URL(${origin}${basePath}${workerPathname})with no query. The concern about the owner page losingsearchParamson re-render is moot: on the forwarded pathshouldSkipPageRenderingis true for non-HTTP-fallback actions (app-server-action-execution.ts:1455-1457), so the page isn't re-rendered — only the actionreturnValueis streamed. This mirrors Next.jsskipPageRendering = actionWasForwarded. The added comment documents the parity intent.
Security model re-confirmed sound:
x-action-forwardedis unspoofable — it's inINTERNAL_HEADERS, sofilterInternalHeadersstrips any client copy beforehandleServerActionRequestsees the request (app-rsc-handler.ts:1316), and it is re-injected only via the trustedctx.actionForwardedpath (:1317-1318). The re-entry loop guard fails closed to{}(app-action-forwarding.ts:149-152).- The cookie-merge divergence from Next.js is deliberate and correct: vinext's
ResponseCookies.getAll()returns deletions asvalue: ""(notundefined), so it can't use Next.js'scookie.value === undefinedcheck. The customdeletesCookie()heuristic (Max-Age<=0/ expiredExpires) is self-consistent with vinext's owndelete()which setsexpires: new Date(0).
Remaining (non-blocking, carry-over)
Eager static manifest cost on every prod build — buildStaticActionOwnerManifest still runs unconditionally in the load hook and AST-parses every route-reachable module, even though in production the Rollup module graph is authoritative and staticOwners is only consulted for the node_modules re-export edge case (action-owner-manifest.ts:358). This lines up with the build-time regression the benchmark bot flagged (+10–12% across rounds). Narrowing the static pass to node_modules re-export chains (or gating it on their presence) would keep the common case off that cost. Not a merge blocker.
Nice work — the injection-guard hardening, the escaping fix, and the ExecutionContext threading all landed cleanly, and the thin-entry delegation is holding up.
|
Review posted to PR #2520. SummaryI reviewed the current head ( All substantive prior-review items are now resolved:
Security model re-confirmed sound: Remaining (non-blocking): the eager No blocking issues found. |
|
/bigbonk review for issues |
Review: server action routing alignment (head
|
|
Review posted to PR #2520. SummaryI reviewed the current head ( All substantive prior-review items remain resolved, and the security model is sound: New commit Non-blocking observations:
No blocking issues found. |
|
/bigbonk review for issues |
99a8a1e to
d596a09
Compare
…ime-takeover # Conflicts: # packages/vinext/src/server/app-server-action-execution.ts # tests/app-browser-server-action-client.test.ts # tests/app-server-action-execution.test.ts
|
/bigbonk review for issues |
|
UnknownError: ProviderInitError |
|
@james-elicx Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
|
/bigbonk review for issues |
|
UnknownError: ProviderInitError |
|
/bigbonk review for issues |
|
UnknownError: ProviderInitError |
|
/bigbonk review for issues |
|
UnknownError: ProviderInitError |
|
/bigbonk review for issues |
|
UnknownError: ProviderInitError |
Summary
importIdsmanager.serverReferences.metaMapbuildApp, without modifying plugin-rsc or adding another parser/lexer passDependency
This now depends only on published
@vitejs/plugin-rscAPIs available from^0.5.31:getPluginApi()manager.clientReferenceMetaMapmanager.serverReferences.metaMapgetModuleInfo().importedIds/dynamicallyImportedIdsgraphsThe local plugin-rsc patch and its scan-build observer API are removed. The lockfile currently resolves
@vitejs/plugin-rscto0.5.32.Reachability Semantics
The manifest is intentionally conservative and module-level, matching the model described in vitejs/vite-plugin-react#1337 and demonstrated by #1341:
Validation
vp checkon the changed action-owner, forwarding, generated-entry, plugin, and test filesvp test run tests/action-owner-manifest.test.ts tests/app-action-forwarding.test.ts tests/action-runtime-security.test.ts tests/app-server-action-execution.test.ts tests/entry-templates.test.ts tests/app-router-rsc-plugin.test.ts— 183 passedgit diff --check origin/main..HEAD