fix(core): establish request context at every server-code entry point

Wraps the three additional entry points where user server code runs
outside fetchHandler's own context wrap:
- the server-function RPC path (/__wrnexus/rpc) intercepted before
  handlers.fetch in the dev server (index.ts) - what server.fn() travels
- the service RPC path (isRpcPath) inside fetchHandler, which runs
  implement()/implementStream() service code before ctx existed
- the HMR-sync handler, which runs real load blocks/actions via dispatch()

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 02:55:18 +05:30
co-authored by Claude Opus 5
parent 680ea73975
commit c7e40154ca
3 changed files with 94 additions and 32 deletions
+29 -2
View File
@@ -8,7 +8,14 @@
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { resolve, dirname, isAbsolute, join } from "node:path";
import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core";
import {
createContext,
runWithRequestContext,
type Middleware,
type Mode,
type SecurityConfig,
type SeoConfig,
} from "@wrnexus/core";
import { buildRouter, type Router } from "@wrnexus/router";
import {
resolveThemeConfig,
@@ -88,6 +95,25 @@ export function validateRpcCsrf(request: Request): boolean {
return Boolean(cookieToken && headerToken && decodeURIComponent(cookieToken) === headerToken);
}
/**
* Wrap the server-function RPC handler so it runs inside the request's
* AsyncLocalStorage context. `/__wrnexus/rpc` is intercepted BEFORE
* `handlers.fetch` (fetchHandler) in the dev server, so a server function
* called via `server.fn()` from the browser runs entirely outside
* fetchHandler's own context wrap. It runs user code directly, so — like
* every other entry point that runs user server code — it needs the request
* context too.
*/
export function withServerFnRequestContext(
handler: (request: Request) => Promise<Response>,
): (request: Request) => Promise<Response> {
return (request: Request) => {
const url = new URL(request.url);
const ctx = createContext(request, url);
return runWithRequestContext(ctx, () => handler(request));
};
}
import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types";
import { createPluginRunner, discoverPlugins, type PluginInput } from "@wrnexus/plugin";
import type { ObservabilityConfig, TenancyConfig } from "@wrnexus/styles";
@@ -603,6 +629,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
},
validateCsrf: validateRpcCsrf,
});
const serverFnRpcHandler = withServerFnRequestContext(rpcHandler);
/*
* Hot rebuilds retain their predecessors (see recycle.ts). Only dev reloads
@@ -628,7 +655,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
...(opts.tls ? { tls: opts.tls } : {}),
fetch(request, server) {
recycle?.recordRequest(Date.now());
if (new URL(request.url).pathname === "/__wrnexus/rpc") return rpcHandler(request);
if (new URL(request.url).pathname === "/__wrnexus/rpc") return serverFnRpcHandler(request);
return handlers.fetch(request, server);
},
websocket: handlers.websocket,