Skip to content

fix(app-router): align server action routing - #2520

Draft
james-elicx wants to merge 33 commits into
mainfrom
codex/fix-action-runtime-takeover
Draft

fix(app-router): align server action routing#2520
james-elicx wants to merge 33 commits into
mainfrom
codex/fix-action-runtime-takeover

Conversation

@james-elicx

@james-elicx james-elicx commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

  • route production Server Action requests through a route whose application graph can load the action, preserving middleware request state and Next.js-compatible forwarding/redirect behavior
  • replace the patched scan observer and import/export reconstruction with a Vinext user-land plugin based on the final Vite module graphs, following vitejs/vite-plugin-react#1341
  • during the RSC build, traverse each Vinext route's pages, layouts, boundaries, slots, and intercepts to collect directly reachable server references and reachable Client Component importIds
  • during the client build, traverse from those Client Component roots and join reachable modules with manager.serverReferences.metaMap
  • emit the completed action-owner manifest as an external ESM sidecar after buildApp, without modifying plugin-rsc or adding another parser/lexer pass

Dependency

This now depends only on published @vitejs/plugin-rsc APIs available from ^0.5.31:

  • getPluginApi()
  • manager.clientReferenceMetaMap
  • manager.serverReferences.metaMap
  • Vite's final getModuleInfo().importedIds / dynamicallyImportedIds graphs

The local plugin-rsc patch and its scan-build observer API are removed. The lockfile currently resolves @vitejs/plugin-rsc to 0.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:

  • if a route reaches a server-reference module, every Server Action exported by that module is associated with the route
  • side-effect imports count as module reachability
  • ordinary value propagation through client helpers works, including an action wrapped in an object before reaching a Client Component
  • this manifest is routing/code-availability metadata, not an authorization boundary; actions still require action-local authentication and authorization
  • build-time action redispatch remains production-only

Validation

  • vp check on the changed action-owner, forwarding, generated-entry, plugin, and test files
  • vp 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 passed
  • production Chromium action-owner E2E — 14 passed, including object-wrapped actions, manual RSC registration, middleware redispatch, shared modules, side-effect imports, dynamic owners, cookies, redirects, and unknown IDs
  • git diff --check origin/main..HEAD

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jul 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2520
npm i https://pkg.pr.new/create-vinext-app@2520
npm i https://pkg.pr.new/@vinext/types@2520
npm i https://pkg.pr.new/vinext@2520

commit: bed09af

@github-actions

github-actions Bot commented Jul 4, 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

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared bed09af against base 255be4e using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 1 regressed · 5 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 134.6 KB 134.7 KB ⚫ +0.0%
Client entry size (gzip) vinext 122.2 KB 122.2 KB ⚫ +0.0%
Dev server cold start vinext 3.03 s 3.05 s ⚫ +0.6%
Production build time vinext 3.18 s 3.26 s 🔴 +2.6%
RSC entry closure size (gzip) vinext 113.8 KB 114.4 KB ⚫ +0.5%
Server bundle size (gzip) vinext 191.4 KB 192.1 KB ⚫ +0.4%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review: server action routing alignment

Reviewed the full diff, ran the targeted suites (app-action-forwarding, action-owner-manifest, action-runtime-security, app-server-action-execution, entry-templates) and vp check on the changed source. Everything passes and the security model is sound: x-action-forwarded is stripped by filterInternalHeaders (it is in INTERNAL_HEADERS) and only re-injected via the trusted ctx.actionForwarded path, so a client cannot spoof it, and the forward loop guard fails closed. The redirect path now re-enters the pipeline via dispatchRedirectRequest so middleware gates the target (the 401 test confirms). Nice work.

A few observations, none blocking:

1. Action-owner injection is gated behind the classification stub filter (fragile coupling)

In renderChunk (packages/vinext/src/index.ts:3646-3714) the action-owner injection is nested inside two guards that exist for the classification manifest:

  • if (!rscClassificationManifest) return null; (:3647)
  • if (!code.includes("__VINEXT_CLASS")) return null; (:3650)

The action-owner stub is only replaced if the chunk also contains __VINEXT_CLASS. Today that holds because generateRscEntry always emits both __VINEXT_CLASS and __VINEXT_ACTION_OWNERS unconditionally into the same RSC entry chunk (entries/app-rsc-entry.ts:524-535, :715). But this is an implicit invariant: if a future codegen change drops/renames __VINEXT_CLASS, or splits the entry, the action-owner stub would silently ship un-replaced. At runtime __VINEXT_ACTION_OWNERS() would then return the literal string "__VINEXT_ACTION_OWNERS_STUB__", which is truthy — so forwardServerActionIfNeeded would treat every action as unowned and 404 it (a hard, silent breakage of all server actions).

The this.error("...failed to inject...") guard only fires when the stub is seen but injection returns null; it does not fire when the whole block is skipped. Consider decoupling the action-owner injection from the __VINEXT_CLASS string filter (e.g. also proceed when rscActionOwnerRoutes && code.includes("__VINEXT_ACTION_OWNERS_STUB__")), and asserting the stub was consumed before the build completes.

2. Dev vs prod ownership divergence through node_modules

buildStaticActionOwnerManifest skips regular import traversal for files under node_modules (action-owner-manifest.ts:394, only re-exports are followed). In production this is only a supplement — the authoritative source is the real Rollup module graph in buildActionOwnerManifest. But in dev, the static manifest is the only source (index.ts:3534, actionOwners: isDevelopmentServe ? actionOwners : undefined). So an action reached through a node_modules intermediary via a normal import (not a re-export) could be considered unowned in dev and get 404'd/mis-forwarded, while working in prod. Worth a code comment or a tracked follow-up, since dev/prod parity is a stated project goal.

3. waitUntil/ExecutionContext not threaded through forwarded dispatch

The forward dispatches with ctx = { actionForwarded: true } (app-rsc-entry.ts:1068). isExecutionContextLike (app-rsc-handler.ts:349) requires a waitUntil method, so this ctx is not treated as an execution context and the handler falls back to getRequestExecutionContext(). That ALS lookup should resolve to the outer request's context since the forward runs synchronously inside runWithRequestContext, so this is likely fine — but it means the CF ExecutionContext is intentionally not forwarded. Confirm after()/waitUntil inside a forwarded action still attaches to the request lifetime on Workers.

4. Minor: redundant matchRoute and dead inline-redirect path

  • handleServerActionRequest calls matchRoute(cleanPathname) twice (app-rsc-entry.ts:1060 and :1079). Small, and off the common request path, but reusable.
  • With the RSC entry always supplying dispatchRedirectRequest, the old inline redirect-render branch (app-server-action-execution.ts:1364+) is dead in production and only exercised by fallback tests. Fine to keep, just added maintenance surface.

5. Minor: cookie-deletion propagation untested

mergeActionForwardCookies (app-action-forwarding.ts:36-44) handles cookie.value === undefined as a delete, but the tests only cover middleware cookie rotation, not deletion. Consider a test asserting a middleware response.cookies.delete(...) removes the cookie from the forwarded request.

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Posted the review to PR #2520. Summary of what I found:

Review outcome

The PR is well-implemented and the security model is sound. I verified:

  • x-action-forwarding cannot be spoofed (stripped by filterInternalHeaders, re-injected only via trusted ctx)
  • The forward loop guard fails closed
  • Redirect targets now re-enter the full pipeline so middleware gates them (confirmed by the 401 test)
  • All targeted suites pass (26 + 51 + 89 tests) and vp check is clean

Issues raised (none blocking)

  1. Fragile coupling (highest concern) — The action-owner manifest injection is nested behind the classification-manifest guards in renderChunk, and specifically gated on code.includes("__VINEXT_CLASS"). It only works because codegen currently always emits both stubs in the same chunk. If that invariant ever breaks, the __VINEXT_ACTION_OWNERS_STUB__ string ships un-replaced and silently 404s every server action (the truthy string defeats the !options.actionOwners guard). The existing this.error won't catch the skip case.

  2. Dev/prod parity gap — Dev relies solely on AST-based static ownership analysis, which skips non-reexport imports through node_modules. Prod uses the real module graph, so an action reached via a node_modules intermediary could diverge.

  3. ExecutionContext not forwarded{ actionForwarded: true } isn't execution-context-like, so waitUntil/after() fall back to ALS lookup during forwarding (probably fine, worth confirming on Workers).

  4. Minor — duplicate matchRoute call; dead inline redirect-render path now only used by fallback tests.

  5. Minor — cookie-deletion propagation in mergeActionForwardCookies is untested.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/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)});`
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Follow-up review — action-owner injection robustness + build cost

Re-reviewed the diff, re-ran tests/app-action-forwarding.test.ts, tests/action-owner-manifest.test.ts, and the full production-build tests/action-runtime-security.test.ts (all green). The security model holds up under scrutiny:

  • The forwarded request carries x-action-forwarded only via the trusted ctx.actionForwarded path in app-rsc-handler.ts:1306-1313 — it is set on filteredHeaders after filterInternalHeaders strips any client-supplied copy, so it cannot be spoofed. On re-entry the loop guard (app-action-forwarding.ts:111) reads it and fails closed to a 404.
  • The dynamic-owner test (action-runtime-security.test.ts:337) confirms a forward to a middleware-gated /admin/* owner is blocked (non-flight → normalized to {}), so the literal-bracket forward pathname never leaks an executed action or garbage params past middleware. That resolves my initial concern about dynamic-route owners.

Two things worth addressing, only the first is more than cosmetic:

1. Reinforce the __VINEXT_ACTION_OWNERS injection guard (agree with prior comment, concrete fix)

The action-owner injection at index.ts:3700 only runs if renderChunk gets past if (!code.includes("__VINEXT_CLASS")) return null; at index.ts:3652. Today both stubs are emitted unconditionally into the same RSC entry chunk (app-rsc-entry.ts:715 + the classification stub), so the coupling holds — but it is an implicit invariant across two independent codegen features. If a future change ever splits the entry or drops __VINEXT_CLASS, the __VINEXT_ACTION_OWNERS_STUB__ string ships un-replaced, and __VINEXT_ACTION_OWNERS() returns the literal stub string. That string is truthy, so forwardServerActionIfNeeded treats every action as unowned (no matching owner pattern) and 404s all server actions — a silent, total breakage.

this.error("...failed to inject...") at index.ts:3712 does not catch this: it only fires when the stub is seen but the regex fails to match, not when the __VINEXT_CLASS pre-filter skips the whole chunk. Recommend a build-completion assertion that fails loudly if rscActionOwnerRoutes is still non-null after the RSC build (i.e., the stub was never consumed). That converts a silent runtime 404 into a hard build error, decoupled from the classification stub.

2. Static owner manifest is computed eagerly on every prod build (build-time cost)

buildStaticActionOwnerManifest runs in the load hook on every build (index.ts:3496), AST-parsing every module reachable from every route. In production this is a supplement — the authoritative source is the real Rollup module graph in buildActionOwnerManifest, and staticOwners is only consulted for the node_modules re-export edge case (action-owner-manifest.ts:312). Traversing and transformWithOxc-parsing all first-party route modules a second time at load time is redundant work that the module graph already covers, and it lines up with the +62.5% production-build-time regression flagged by the benchmark bot. Consider narrowing the static pass to only what the module graph can't see (node_modules re-export chains), or gating it behind the presence of such chains, so the common case doesn't pay for it.

Neither blocks merge. Nice work on the security hardening and the thin-entry delegation.

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Posted the review to PR #2520.

Review summary

I re-reviewed the full diff, re-ran the targeted suites (app-action-forwarding, action-owner-manifest) and the full production-build action-runtime-security test — all pass. The PR is correct and well-tested, and the security model is sound:

  • x-action-forwarded cannot be spoofed — stripped by filterInternalHeaders, re-injected only via the trusted ctx.actionForwarded path (app-rsc-handler.ts:1306-1313); the loop guard fails closed to 404.
  • Dynamic-route owners are safe — a forward to a middleware-gated owner is blocked and normalized to {} before the literal-bracket pathname could leak params (confirmed by action-runtime-security.test.ts:337). This resolved my initial concern.

Issues raised (none blocking)

  1. Fragile action-owner injection coupling (highest concern) — The __VINEXT_ACTION_OWNERS injection is gated behind the classification stub's !code.includes("__VINEXT_CLASS") pre-filter (index.ts:3652). It works only because both codegen features emit into the same chunk today. If that invariant ever breaks, the stub ships un-replaced, __VINEXT_ACTION_OWNERS() returns a truthy literal string, and every server action silently 404s. The existing this.error guard doesn't catch the skip case. Recommended a build-completion assertion that fails loudly if the stub was never consumed.

  2. Eager static manifest costbuildStaticActionOwnerManifest AST-parses every route-reachable module on every prod build, even though the module graph is authoritative in prod and staticOwners is only needed for the node_modules re-export edge case. Lines up with the +62.5% build-time regression flagged by the benchmark bot; suggested narrowing the static pass.

This complements the prior ask-bonk review (which also flagged #1, dev/prod parity, ExecutionContext, and untested cookie-deletion) rather than duplicating it.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@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.

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-forwarded cannot be spoofed — it is in INTERNAL_HEADERS (headers.ts:211), so filterInternalHeaders strips any client copy (app-rsc-handler.ts:1316) and it is re-injected only via the trusted ctx.actionForwarded path (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_CLASS coupling) is fixed. renderChunk now gates the action-owner injection on its own independent hasActionOwnerStub condition (index.ts:3664-3666) rather than piggybacking the classification pre-filter, and generateBundle (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 fixedtests/app-action-forwarding.test.ts:133-183 now covers Max-Age=0, Max-Age=-1, and expired-Expires deletions.

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:708function __VINEXT_ACTION_OWNERS() { return ${JSON.stringify(actionOwners)}; }
  • Prod path: injectActionOwnerManifest at action-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.

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2520.

Summary

I reviewed the current head (f698182 — 4 commits past the first review pass), read the full diff, and re-ran the targeted suites (app-action-forwarding, action-owner-manifest, production-build action-runtime-security) — 42 tests pass.

Security model confirmed sound, and two prior review items are now resolved:

Issues raised (only #1 is more than cosmetic):

  1. CodeQL alerts are legitimate — the action-owner manifest is embedded into generated JS via raw JSON.stringify at two sites (app-rsc-entry.ts:708 dev path, action-owner-manifest.ts:377 prod injector). Keys/values are filesystem- and route-derived strings that JSON.stringify doesn't escape for <, >, &, U+2028/U+2029. The repo already has safeJsonStringify (server/html.ts) used everywhere else for this exact purpose. Clean mechanical fix: swap both sites.
  2. Forwarded dispatch drops the query string (forwardUrl.search = "") — fine for the action itself, but the owner-page re-render loses searchParams, a potential dev/prod behavior split. Worth a comment/test or Next.js parity confirmation.
  3. (Carry-over) Eager static manifest costbuildStaticActionOwnerManifest runs unconditionally on every prod build, matching the +11.8% build-time regression the bot flagged. Non-blocking.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@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.

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 pass
  • tests/action-runtime-security.test.ts + tests/app-server-action-execution.test.ts + tests/entry-templates.test.ts — 140 pass
  • tests/app-browser-server-action-client.test.ts — 3 pass
  • vp check on 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_CLASS coupling) — fixed. renderChunk now gates action-owner injection on its own independent hasActionOwnerStub condition (index.ts:3664-3666), and generateBundle (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 safeJsonStringify instead of raw JSON.stringify: the dev path (app-rsc-entry.ts:709) and the prod injector (action-owner-manifest.ts:378). safeJsonStringify escapes <, >, &, 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 expired Expires (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 passes waitUntil/cache/passThroughOnException alongside actionForwarded: true, so isExecutionContextLike (app-rsc-handler.ts:349) recognizes it and after()/waitUntil inside a forwarded action attach to the request lifetime.
  • Prior #2 (dropped query string) — verified against Next.js and now correct + documented. forwardServerActionIfNeeded clears forwardUrl.search (app-action-forwarding.ts:159), matching Next.js createForwardedActionResponse() which builds new URL(${origin}${basePath}${workerPathname}) with no query. The concern about the owner page losing searchParams on re-render is moot: on the forwarded path shouldSkipPageRendering is true for non-HTTP-fallback actions (app-server-action-execution.ts:1455-1457), so the page isn't re-rendered — only the action returnValue is streamed. This mirrors Next.js skipPageRendering = actionWasForwarded. The added comment documents the parity intent.

Security model re-confirmed sound:

  • x-action-forwarded is unspoofable — it's in INTERNAL_HEADERS, so filterInternalHeaders strips any client copy before handleServerActionRequest sees the request (app-rsc-handler.ts:1316), and it is re-injected only via the trusted ctx.actionForwarded path (: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 as value: "" (not undefined), so it can't use Next.js's cookie.value === undefined check. The custom deletesCookie() heuristic (Max-Age<=0 / expired Expires) is self-consistent with vinext's own delete() which sets expires: new Date(0).

Remaining (non-blocking, carry-over)

Eager static manifest cost on every prod buildbuildStaticActionOwnerManifest 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.

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2520.

Summary

I reviewed the current head (2dcf18a, 5 commits — one past the prior ask-bonk pass), read the full diff, re-ran the targeted suites (177 tests across 6 files, all pass) and vp check (clean).

All substantive prior-review items are now resolved:

  1. Fragile __VINEXT_CLASS coupling — fixed with an independent hasActionOwnerStub guard plus a generateBundle build-fail assertion, converting a silent runtime 404 into a hard build error.
  2. CodeQL "improper code sanitization" — fixed; both manifest-injection sites now use safeJsonStringify (escapes <, >, &, U+2028/U+2029), with a new test.
  3. ExecutionContext not forwarded — now threads real waitUntil/cache/passThroughOnException through the forward dispatch.
  4. Dropped query string — verified against Next.js source (createForwardedActionResponse also drops the query) and confirmed moot because forwarded non-fallback actions skip page rendering (shouldSkipPageRendering), matching Next.js skipPageRendering = actionWasForwarded.
  5. Untested cookie deletion — now covered (Max-Age=0, Max-Age=-1, expired Expires).

Security model re-confirmed sound: x-action-forwarded is unspoofable (in INTERNAL_HEADERS, stripped then re-injected only via trusted ctx; loop guard fails closed). I also verified the cookie-merge divergence from Next.js is deliberate and self-consistent given vinext's ResponseCookies model.

Remaining (non-blocking): the eager buildStaticActionOwnerManifest still runs on every prod build, matching the benchmark bot's build-time regression. Suggested narrowing it to node_modules re-export chains.

No blocking issues found.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review: server action routing alignment (head 577afff, 6 commits)

Reviewed the current head, which is one commit (577afff "parse route parameters linearly") past the prior ask-bonk pass. Re-ran the targeted suites and independently verified the security model and the new commit.

  • tests/app-action-forwarding.test.ts + tests/action-owner-manifest.test.ts — 34 pass
  • tests/action-runtime-security.test.ts + tests/entry-templates.test.ts — 51 pass
  • tests/route-sorting.test.ts — 48 pass

All substantive items from prior reviews remain resolved (independent hasActionOwnerStub guard + generateBundle build-fail assertion at index.ts:3664-3666/:3744-3750; safeJsonStringify at both injection sites; ExecutionContext threading; cookie-deletion tests; documented query-string drop). Security model re-confirmed sound: x-action-forwarded is in INTERNAL_HEADERS (headers.ts:211), stripped by filterInternalHeaders (request-pipeline.ts:594) at app-rsc-handler.ts:1316, and re-injected only via the trusted isForwardedActionContext(ctx) path (:1317-1318); the re-entry loop guard fails closed to {} (app-action-forwarding.ts:149-152).

New commit 577afff — ReDoS fix in patternToNextFormat (verified correct)

The regex rewrite to a linear split("/").map(...) (route-validation.ts:150-163) is a genuine ReDoS fix — the old :([^/]+?)\+(?=\/|$) patterns could backtrack on adversarial input, which the new handles adversarial parameter names in linear time test (route-sorting.test.ts:178) guards against. I diffed old vs. new behavior across edge cases and confirmed the only behavioral divergences are unreachable from real route generation:

  • /: → old /:, new /[]; /:+ → old /[+], new /[...]; /:* → old /[*], new /[[...]]; /a:b → old /a[b], new /a:b.
  • The route scanners always emit param segments as a whole :-prefixed segment (app-route-graph.ts:2920/2931/2940, pages-router.ts:132/144/154, route-pattern.ts:7/10/13), never a bare : or a static-prefixed segment like a:b, and never a nameless param.
  • The rest of the routing code already keys param detection on segment.startsWith(":") / patternPart.startsWith(":") (route-pattern.ts:103/121/154/157, app-route-graph.ts:2196/2233/2991), so the new segment-anchored approach is more consistent with the codebase than the old mid-segment-colon regex.
  • All prior parity cases still pass (:c++lang[c++lang], :a*b[a*b], :sign-in+[...sign-in]). The forwarding path (app-action-forwarding.ts:156) feeds it route.pattern, which is always well-formed.

No parity regression. Good hardening.

Non-blocking observations

  1. Eager static manifest cost (carry-over). buildStaticActionOwnerManifest is gated on hasServerActions (index.ts:3509) — good — but for server-action apps it still AST-parses every route-reachable module on every prod build even though the Rollup module graph is authoritative there and staticOwners is only consulted for the node_modules re-export edge case. Lines up with the benchmark bot's build-time regression. Narrowing the static pass to node_modules re-export chains would keep the common case off that cost.

  2. deletesCookie Expires <= now boundary (cosmetic). mergeActionForwardCookies treats a cookie whose Expires equals the captured now as a deletion (app-action-forwarding.ts:51). This is the correct/expected reading and vinext's own delete() uses new Date(0), so it's self-consistent; noting only that the boundary is inclusive.

No blocking issues found. The ReDoS fix, injection-guard hardening, escaping fix, and ExecutionContext threading all landed cleanly.

@ask-bonk

ask-bonk Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2520.

Summary

I reviewed the current head (577afff, 6 commits — one past the prior ask-bonk pass), re-ran the targeted suites (route-sorting, app-action-forwarding, action-owner-manifest, action-runtime-security, entry-templates — all pass), and independently verified the security model and the new commit.

All substantive prior-review items remain resolved, and the security model is sound: x-action-forwarded is unspoofable (in INTERNAL_HEADERS, stripped by filterInternalHeaders, re-injected only via the trusted ctx path; loop guard fails closed).

New commit 577afff rewrites patternToNextFormat from a backtracking-prone regex to a linear split/map parser — a legitimate ReDoS fix. I diffed old-vs-new behavior across edge cases and confirmed the only divergences (/:, /:+, /:*, /a:b) are unreachable from real route generation: the route scanners always emit whole :-prefixed param segments, and the rest of the routing code already keys on segment.startsWith(":"). All parity cases still pass. No regression.

Non-blocking observations:

  1. Eager static manifest cost (carry-over) — gated on hasServerActions but still full AST-parses on every prod build for action apps.
  2. deletesCookie uses an inclusive Expires <= now boundary — correct and self-consistent, noted only.

No blocking issues found.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

UnknownError: ProviderInitError

github run

@ask-bonk

ask-bonk Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

UnknownError: ProviderInitError

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

UnknownError: ProviderInitError

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

UnknownError: ProviderInitError

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

UnknownError: ProviderInitError

github run

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.

2 participants