Skip to content

Instantly share code, notes, and snippets.

@hi-ogawa
Created July 30, 2026 00:57
Show Gist options
  • Select an option

  • Save hi-ogawa/4f88a7f6e51c04b37eb25c81a9faae21 to your computer and use it in GitHub Desktop.

Select an option

Save hi-ogawa/4f88a7f6e51c04b37eb25c81a9faae21 to your computer and use it in GitHub Desktop.
Client-directed server action dispatch research

Client-Directed Action Dispatch

  • Repo: git@github.com:vitejs/vite-plugin-react.git
  • Commit: 32b9023fa34ea4fe4b3081ce7bddc1d7a86535f5
  • Branch: route-scoped-deployment
  • Worktree: /Users/hogawa/code/others/vite-plugin-react-route-scoped-deployment

Purpose

Explore client-directed action dispatch as an alternative to server-selected redispatch for route-scoped RSC deployments.

This is downstream of issue #1337, not a replacement for it. The higher-level problem remains deriving route-to-server-reference availability and producing route-scoped deployment artifacts. Once references are no longer globally available, every invocation must also reach a deployment that can load its outer action. Dispatch is therefore a required consequence of the deployment model, but its transport and rendering semantics are a separate design problem.

Current Prototype

Merged PR #1341 first demonstrated action routing with one global server build. Draft PR #1348 extends that example with filtered route deployment artifacts.

The PR #1348 production flow is:

browser posts retained action A to current route B
  -> canonical server router looks up A in route/action manifest
  -> router rewrites the request pathname to route A
  -> router loads deployment A and invokes its handler in process
  -> deployment A executes action A through route A middleware
  -> response continues rendering visible route B

This is not a loopback HTTP self-fetch, so it does not reproduce Next.js's internal-origin Host bug. The router invokes the selected handler directly and keeps the original request origin. However, it still puts target selection and cross-route request reconstruction on the server. A physically separated deployment would need an actual forwarding transport or an external platform router unless the browser sends the request to the correct deployment directly.

Next.js Direction

Next.js PR #93792 fixes an infinite forwarding loop but explicitly calls server-side action forwarding a short-term solution. It points to draft PR #90549, which experiments with removing server forwarding and dispatching from the client to the pathname captured by the action proxy.

Issue #96344 adds a production reason to avoid internal HTTP forwarding: the loopback fetch changes Host to localhost:PORT, which breaks applications deriving tenant identity from the public host. Issue #84504 previously showed that middleware rewrites could create an infinite forwarding loop.

The Next.js experiment is not complete. Its CI currently fails delayed-action result, redirect, navigation, interception, parallel-route revalidation, and other tests. It proves that direct client dispatch is an active direction, but it also exposes the unresolved split between action execution and render reconciliation.

See NEXTJS.md for the detailed source trace and status.

Two Targets

After a client retains action A and navigates from route A to route B, one invocation has two distinct targets:

execution target = deployment A, because it can load action A
render target = route B, because it remains visible in the browser

Routing the POST to A solves only execution. Applying A's returned Flight tree to the browser would incorrectly replace or patch B using stale route context. Discarding A's Flight tree preserves B, but then revalidation and refreshed data for B need another defined path.

A complete protocol must answer:

  • How is the action return value delivered independently of route Flight data?
  • When does a successful action trigger a refresh of the current route?
  • Which route computes revalidation and cache invalidation?
  • How are redirects interpreted when execution and render routes differ?
  • How are cookies and middleware mutations from the execution route applied?
  • How are parallel slots, intercepted routes, and optimistic state reconciled?
  • Can one response carry an action result plus a render request for another route, or are two requests required?

This is the central complication in client-directed dispatch. It is not enough to choose the correct action endpoint.

Possible Client Target Models

Reference-Bound Dispatch Path

Bind a dispatch pathname when each client server-reference proxy is created or decoded:

reference A created while route A is active
  -> retain { action ID A, dispatch path /a }
  -> later invocation posts directly to /a

This resembles Next.js draft PR #90549. It naturally preserves the route associated with a retained reference, but the meaning of creation time needs definition for shared modules, module caching, RSC-decoded references, and references passed through runtime state.

Client Manifest Lookup

Ship an action ID to compatible route list to the browser and select a target when invoked:

action ID A -> [/a, /shared]

This directly reuses the build-time route/action relation and does not depend on proxy creation timing. When several routes are compatible, the client can use the same stable first-compatible-route policy as server-side manifest routing.

Server-Issued Dispatch Capability

Serialize a framework-defined dispatch target or opaque capability alongside a server reference when rendering it. The browser returns that target on invocation.

This can represent runtime provenance more directly than static module ownership, but React's server-reference wire format and generated client proxies do not currently expose an obvious framework field for arbitrary dispatch metadata. It also raises signing, deployment-version, and replay questions.

These models may coexist. Static imported proxies and references decoded from RSC payloads need not acquire their target through the same mechanism, but their observable invocation semantics should agree.

plugin-RSC Seams

plugin-RSC currently transforms client-side "use server" proxies to:

createServerReference(action ID, global callServer, ...)

The browser runtime's setServerCallback() installs one global callback receiving only (id, args). This supports client-manifest lookup but does not bind a distinct target to each generated proxy.

React references decoded through createFromReadableStream() or createFromFetch() can receive a custom callServer option. A framework could therefore decode each route response with a callback bound to that response's route. Imported proxy modules would still need either a transformed bound callback or invocation-time manifest lookup.

An initial experiment should stay framework-local. A plugin-RSC API should be proposed only after the example identifies a semantic fact that cannot be expressed through existing transform hooks, decode options, and browser callbacks.

Proposed Experiment

Retain PR #1348's route-scoped packaging and filtered registries, but replace server-selected action routing for the retained-action scenario:

  1. Make the browser know a compatible dispatch route for action A.
  2. Visit route A and retain action A.
  3. Navigate to route B.
  4. POST action A directly to deployment A rather than posting to B and redispatching on the server.
  5. Execute A through route A middleware.
  6. Keep route B visible.
  7. Return the action value without applying route A's Flight tree to B.
  8. Define and test whether route B refreshes after the action.
  9. Reject a crafted action A request sent directly to deployment B.
  10. Inspect deployment artifacts to retain the original #1337 availability proof.

The smallest first test can use an action with no revalidation and return a scalar result. A second test must add current-route revalidation because otherwise the experiment avoids the main unresolved protocol problem demonstrated by Next.js PR #90549.

Protocol Boundaries

Nested Server References

Directing the outer action to a compatible deployment does not guarantee that deployment can decode server references nested in its arguments. The selected deployment would need all nested references, a broader resolver, preserved remote proxies, or an explicit rejection rule. This is the same limitation reproduced in Next.js issue #96331.

Progressive Forms

A natural progressive form submission posts to the route that rendered the form, so it usually already reaches a compatible deployment. Crafted cross-route progressive requests still require route-local validation of multipart-embedded action IDs if the framework wants fail-closed behavior. Client-directed hydrated dispatch does not solve this path.

Authorization

Dispatch metadata identifies code availability, not user authority. A client can observe or forge a target pathname, and one action can be available from routes with different middleware. Route rejection and middleware re-entry are defense in depth. Authentication and resource authorization remain action-local requirements.

Implication for #1337

The new perspective does not weaken the motivation for route-aware build metadata. It sharpens the separation of responsibilities:

#1337 build problem
  -> which deployments can load each reference?
  -> how are filtered artifacts produced?

dispatch protocol problem
  -> how does an invocation reach one compatible deployment?
  -> how does its result reconcile with the currently rendered route?

PR #1348 remains useful as a packaging prototype, but its server-side router should not become the implied recommended transport without comparing it to client-directed dispatch. The next research step is to prototype the two-target client flow and treat current-route revalidation as a required acceptance criterion rather than an optional follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment