Repository navigation
Server-function build id never reaches the server when the host sets builder.sharedConfigBuild (nitro/vite) #511
Description
Versions
- @solidjs/vite-plugin 3.0.0-next.53
- @solidjs/web 2.0.0-rc.20
- nitro 3.0.261029-rc (
nitro/vite, presetvercel; other presets should behave the same) - Vite+ 1.4.0
Reproduction
// vite.config.ts
plugins: [
solid({ start: {}, ssr: true, serverFunctions: true }),
nitro({ preset: 'vercel' })
]Add at least one 'use server' function and run vite build.
Expected
The client chunk stamps X-Server-Function-Build: <id> and the generated handler calls
configureServerFunctionsServer({ ..., buildId: '<id>' }), so a call from another build gets the 409 added in #492
(solidjs/solid#4215).
Observed
The client stamps the id and the persisted manifest is written to dist/.vite/solid-server-functions.json with that
buildId, but the handler is emitted as
configureServerFunctionsServer({ provideEvent: provideRequestEvent, endpoint: "/_server" }), so the check never runs.
Cause (dist/esm/index.mjs of next.53)
configResolvedstoresoutDir = config.build.outDir(L1933) and the clientwriteBundlewrites the manifest there
(L1965, L1395-1396). nitro setsbuilder: { sharedConfigBuild: true }(nitro/dist/vite.mjs:592), so
configResolvedruns once on the top-level config:outDirisdist, not the client environment's output
(nitro moves it tonitro.options.output.publicDirinconfigEnvironment,vite.mjs:500-502).readPersistedFileonly readspath.resolve(root, 'dist/client', '.vite/solid-server-functions.json')(L1377), used
byreadPersistedBuildIdwhen the handler loads (L1643).
Build order is fine: solid:client-build-first builds the client before nitro's post-order buildApp.
Consequence
- Skew protection is silently off for every nitro build.
- A stale
dist/client/.vite/solid-server-functions.jsonleft by an earlier build is read instead. Reproduced in the
build output: the client stamped5d94d82bbcf65710while the generated handler was configured with the stale
buildId: "e7399f5b6259c6a7". Per@solidjs/webserver.js(thecallerBuild !== config.buildIdbranch), every
stamped call from that build is then answered 409.
Suggested fix
When the client was built by the same plugin instance (builder mode), use the in-memory buildId. Otherwise record
this.environment.config.build.outDir in writeBundle and read from it first, falling back to dist/client, like
clientManifestPath() (L5180-5190). Writing the file into nitro's client outDir would publish it under /.vite/, so the
in-memory path is preferable. Invalidating the persisted file at the start of the client build would also stop a stale
id from being read. The test added in #492 only covers the two-invocation build; #381 was a similar hard-coded path
under nitro.
Related: stamping is dropped when the app configures the client through the /client subpath
clientRuntimePlugin only redirects source === runtime.client (@solidjs/web/server-functions, L1733). An app that
calls configureServerFunctionsClient({ prepareRequest }) imported from @solidjs/web/server-functions/client (the
import the README recommends, README L987-990) reaches client.js directly; client.js:768 replaces
config.prepareRequest, dropping the build stamp without a warning. Either cover ${runtime.client}/client in
resolveId or compose the stamp inside the runtime.
Activity
everton-dgn commentedon Oct 31, 2026
Thanks for the quick fix in #514. With 3.0.0-next.54 the handler under nitro gets the in-memory build id, and the client stamp and the server
buildIdmatch in our build.One follow-up, low priority: the client
writeBundlenow writes.vite/solid-server-functions.jsonto the directory the client build actually wrote (writtenClientOutDir,dist/esm/index.mjsL2080-2082). Under nitro that isnitro.options.output.publicDir(nitro/dist/vite.mjsL502), so with the vercel preset the file ends up in.vercel/output/static/.vite/and the CDN serves it at/.vite/solid-server-functions.json. In an app with server functions it lists the root-relative paths of the server modules alongside the build id (writePersistedManifest, L1461).In builder mode the server build no longer reads that file (
builtClient ? buildId : readPersistedBuildId(...), L1735), so it could be skipped there or written outside the public output. The Vitemanifest.jsonthe plugin enables for the client build (L3089, L3100) lands in the same.vite/directory and is published the same way.We work around it by moving
.vite/out of the public directory in a nitrocompiledhook after the build.