From 2f0384a0812e2e15e81a150690a658ead83ef913 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Thu, 27 Aug 2026 01:31:30 -0700 Subject: [PATCH 1/2] Route path-addressed server function calls solidjs/solid#3076 moved the function id into the path (`/`), retiring the X-Server-Function-Id header and the `?id=` query fallback. The dev middleware and the generated dispatchRequest gate matched the endpoint pathname exactly, so the new addresses fell through to SSR; both now match by mount prefix (exact or `endpoint + '/'`). The dev middleware's module-preload id comes from the path segment, with the header and `?id=` forms kept as fallbacks for a client runtime older than the addressing change. Verified end-to-end against the fullstack-tanstack template with the solid `next` runtime linked in: GET-declared reads (GET and HEAD), 405/403 gating, and the unscripted no-JS form POST flow (303 + flash) all dispatch through the new addresses in dev. Co-authored-by: Cursor --- .changeset/path-addressed-server-functions.md | 5 +++ src/server-functions/index.ts | 35 +++++++++++++++---- src/ssr/index.ts | 6 +++- 3 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 .changeset/path-addressed-server-functions.md diff --git a/.changeset/path-addressed-server-functions.md b/.changeset/path-addressed-server-functions.md new file mode 100644 index 0000000..87552ae --- /dev/null +++ b/.changeset/path-addressed-server-functions.md @@ -0,0 +1,5 @@ +--- +"@solidjs/vite-plugin": minor +--- + +Route path-addressed server function calls (solidjs/solid#3076). A call's address is now `/` with arguments in the query, so the dev middleware and the generated request-dispatch gate match the endpoint by mount prefix instead of exact pathname, and the dev middleware's module-preload id comes from the path segment (with the retired `X-Server-Function-Id` header and `?id=` forms kept as fallbacks for older client runtimes). Requires `@solidjs/web` newer than 2.0.0-rc.3 for the addressing itself; older runtimes keep working through the exact-match and fallback paths. diff --git a/src/server-functions/index.ts b/src/server-functions/index.ts index faeac2f..4344c5c 100644 --- a/src/server-functions/index.ts +++ b/src/server-functions/index.ts @@ -515,27 +515,48 @@ export function serverFunctions( if (internal.externalDevServer || !isRunnableEnvironment(ssrEnvironment)) { return; } + // A call's address is `/` (solidjs/solid#3076) — the + // mount plus exactly one path segment. Bare-mount requests still + // reach the runtime handler (it answers 404), so misdirected posts + // fail through the endpoint rather than falling through to SSR. + const underMount = (pathname: string, mount: string) => + pathname === mount || pathname.startsWith(mount + '/'); server.middlewares.use((req, res, next) => { const url = new URL(req.url || '/', 'http://localhost'); // Match with and without `base` — middleware-mode hosts may mount // vite.middlewares below the base themselves. - if (url.pathname !== resolvedEndpoint && url.pathname !== endpoint) { + if (!underMount(url.pathname, resolvedEndpoint) && !underMount(url.pathname, endpoint)) { return next(); } + const basePrefixed = underMount(url.pathname, resolvedEndpoint); // When the stripped form matched, restore the base for dispatch: // the generated handler compares the request pathname against the // base-prefixed endpoint, and production handlers only ever see // base-prefixed URLs. - const dispatchUrl = - url.pathname === resolvedEndpoint ? undefined : joinBase(base, req.url || '/'); + const dispatchUrl = basePrefixed ? undefined : joinBase(base, req.url || '/'); (async () => { // Make sure the referenced module has been evaluated in the SSR // environment so its registration exists — functions only client // code references are never loaded by the SSR render itself. - const headerId = req.headers['x-server-function-id']; - const functionId = - (typeof headerId === 'string' ? headerId.split('#')[0] : undefined) || - url.searchParams.get('id'); + // The id lives in the path segment after the mount; the header + // and `?id=` forms stay as fallbacks for a client runtime older + // than the addressing change. + const mount = basePrefixed ? resolvedEndpoint : endpoint; + const segment = url.pathname.slice(mount.length + 1); + let functionId: string | null = null; + if (segment && !segment.includes('/')) { + try { + functionId = decodeURIComponent(segment); + } catch { + // not an address; the runtime handler answers the 404 + } + } + if (!functionId) { + const headerId = req.headers['x-server-function-id']; + functionId = + (typeof headerId === 'string' ? headerId.split('#')[0] : undefined) || + url.searchParams.get('id'); + } if (functionId) { const entry = moduleForFunctionId(functionId); if (entry) await ssrEnvironment.runner.import(moduleDevUrl(entry)); diff --git a/src/ssr/index.ts b/src/ssr/index.ts index 14d700d..d2609d5 100644 --- a/src/ssr/index.ts +++ b/src/ssr/index.ts @@ -1002,7 +1002,11 @@ export function startServe( lines.push(``, `async function dispatchRequest(request, event, options) {`); if (composeServerFunctions) { lines.push( - ` if (new URL(request.url).pathname === endpoint) {`, + // A call's address is `/` (solidjs/solid#3076); the + // bare mount still routes so a misaddressed request 404s through the + // runtime handler instead of rendering a page at it. + ` const requestPath = new URL(request.url).pathname;`, + ` if (requestPath === endpoint || requestPath.startsWith(endpoint + '/')) {`, // The call shares the middleware chain's event (locals decoration, // the response stub); an explicit host-provided createEvent wins. // No fold here: the runtime's server-function handler runs the From 027ff5062766d93c87fde4d8999a66e68d589bd2 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Thu, 27 Aug 2026 01:44:13 -0700 Subject: [PATCH 2/2] Mark legacy addressing fallbacks as RC-transitional, to drop before stable Co-authored-by: Cursor --- .changeset/path-addressed-server-functions.md | 2 +- src/server-functions/index.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.changeset/path-addressed-server-functions.md b/.changeset/path-addressed-server-functions.md index 87552ae..d92dd4f 100644 --- a/.changeset/path-addressed-server-functions.md +++ b/.changeset/path-addressed-server-functions.md @@ -2,4 +2,4 @@ "@solidjs/vite-plugin": minor --- -Route path-addressed server function calls (solidjs/solid#3076). A call's address is now `/` with arguments in the query, so the dev middleware and the generated request-dispatch gate match the endpoint by mount prefix instead of exact pathname, and the dev middleware's module-preload id comes from the path segment (with the retired `X-Server-Function-Id` header and `?id=` forms kept as fallbacks for older client runtimes). Requires `@solidjs/web` newer than 2.0.0-rc.3 for the addressing itself; older runtimes keep working through the exact-match and fallback paths. +Route path-addressed server function calls (solidjs/solid#3076). A call's address is now `/` with arguments in the query, so the dev middleware and the generated request-dispatch gate match the endpoint by mount prefix instead of exact pathname, and the dev middleware's module-preload id comes from the path segment. The retired `X-Server-Function-Id` header and `?id=` forms remain as transitional fallbacks for the RC window only — they will be dropped before the stable release. diff --git a/src/server-functions/index.ts b/src/server-functions/index.ts index 4344c5c..49c1cf3 100644 --- a/src/server-functions/index.ts +++ b/src/server-functions/index.ts @@ -538,9 +538,7 @@ export function serverFunctions( // Make sure the referenced module has been evaluated in the SSR // environment so its registration exists — functions only client // code references are never loaded by the SSR render itself. - // The id lives in the path segment after the mount; the header - // and `?id=` forms stay as fallbacks for a client runtime older - // than the addressing change. + // The id lives in the path segment after the mount. const mount = basePrefixed ? resolvedEndpoint : endpoint; const segment = url.pathname.slice(mount.length + 1); let functionId: string | null = null; @@ -552,6 +550,10 @@ export function serverFunctions( } } if (!functionId) { + // TRANSITIONAL (remove before 3.0 stable): the retired header + // and `?id=` addressing, kept only for the RC window where this + // plugin meets a @solidjs/web older than the path-addressing + // change (solidjs/solid#3076). const headerId = req.headers['x-server-function-id']; functionId = (typeof headerId === 'string' ? headerId.split('#')[0] : undefined) ||