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:
@@ -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,
|
||||
|
||||
@@ -1048,22 +1048,28 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
withSecurityHeaders(req, res, mode, runtimeSecurity, nonce);
|
||||
|
||||
if (isRpcPath(url.pathname)) {
|
||||
let services: Map<string, ServiceImplementation | StreamImplementation>;
|
||||
try {
|
||||
services = await loadServices();
|
||||
} catch (error) {
|
||||
const app = process.env.WRNEXUS_APP_NAME ?? "app";
|
||||
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
||||
console.error(`[wrnexus] RPC request failed to load services (${app})\n${detail}`);
|
||||
return secure(
|
||||
Response.json(
|
||||
{ ok: false, code: "RPC_UNKNOWN", message: "Service unavailable", retryable: false },
|
||||
{ headers: { "cache-control": "private, no-store" } },
|
||||
),
|
||||
);
|
||||
}
|
||||
const rpcResponse = await handleRpcRequest(req, url, services);
|
||||
if (rpcResponse) return secure(rpcResponse);
|
||||
// Service RPC handlers run user code (implement()/implementStream()), so
|
||||
// they need the same request context an ordinary API route gets.
|
||||
const rpcCtx = createContext(req, url);
|
||||
const rpcOutcome = await runWithRequestContext(rpcCtx, async () => {
|
||||
let services: Map<string, ServiceImplementation | StreamImplementation>;
|
||||
try {
|
||||
services = await loadServices();
|
||||
} catch (error) {
|
||||
const app = process.env.WRNEXUS_APP_NAME ?? "app";
|
||||
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
||||
console.error(`[wrnexus] RPC request failed to load services (${app})\n${detail}`);
|
||||
return secure(
|
||||
Response.json(
|
||||
{ ok: false, code: "RPC_UNKNOWN", message: "Service unavailable", retryable: false },
|
||||
{ headers: { "cache-control": "private, no-store" } },
|
||||
),
|
||||
);
|
||||
}
|
||||
const rpcResponse = await handleRpcRequest(req, url, services);
|
||||
return rpcResponse ? secure(rpcResponse) : undefined;
|
||||
});
|
||||
if (rpcOutcome) return rpcOutcome;
|
||||
}
|
||||
|
||||
const preflight = createCorsPreflightResponse(req, deps.security);
|
||||
@@ -2179,21 +2185,23 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
headers.set("x-wrnexus-hmr", "1");
|
||||
const req = new Request(url, { headers });
|
||||
const ctx = createContext(req, url);
|
||||
initializeRequestCache(ctx);
|
||||
ctx.locals.cspNonce = randomNonce();
|
||||
if (deps.i18n) {
|
||||
ctx.lang = resolveLang(
|
||||
deps.i18n,
|
||||
ctx.cookies.get(deps.i18n.cookie.name),
|
||||
req.headers.get("accept-language"),
|
||||
const html = await runWithRequestContext(ctx, async () => {
|
||||
initializeRequestCache(ctx);
|
||||
ctx.locals.cspNonce = randomNonce();
|
||||
if (deps.i18n) {
|
||||
ctx.lang = resolveLang(
|
||||
deps.i18n,
|
||||
ctx.cookies.get(deps.i18n.cookie.name),
|
||||
req.headers.get("accept-language"),
|
||||
);
|
||||
ctx.t = makeT(deps.i18n, ctx.lang);
|
||||
}
|
||||
const res = withContextHeaders(
|
||||
ctx,
|
||||
await runMiddleware(await resolveMiddleware(), ctx, () => dispatch(ctx)),
|
||||
);
|
||||
ctx.t = makeT(deps.i18n, ctx.lang);
|
||||
}
|
||||
const res = withContextHeaders(
|
||||
ctx,
|
||||
await runMiddleware(await resolveMiddleware(), ctx, () => dispatch(ctx)),
|
||||
);
|
||||
const html = await res.text();
|
||||
return res.text();
|
||||
});
|
||||
ws.send(JSON.stringify({ type: "html", html }));
|
||||
} catch (err) {
|
||||
if (mode === "development") console.error(err);
|
||||
|
||||
Reference in New Issue
Block a user