Skip to content

Server-function build id never reaches the server when the host sets builder.sharedConfigBuild (nitro/vite) #511

Closed

Description

@everton-dgn

Versions

  • @solidjs/vite-plugin 3.0.0-next.53
  • @solidjs/web 2.0.0-rc.20
  • nitro 3.0.261029-rc (nitro/vite, preset vercel; 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)

  1. configResolved stores outDir = config.build.outDir (L1933) and the client writeBundle writes the manifest there
    (L1965, L1395-1396). nitro sets builder: { sharedConfigBuild: true } (nitro/dist/vite.mjs:592), so
    configResolved runs once on the top-level config: outDir is dist, not the client environment's output
    (nitro moves it to nitro.options.output.publicDir in configEnvironment, vite.mjs:500-502).
  2. readPersistedFile only reads path.resolve(root, 'dist/client', '.vite/solid-server-functions.json') (L1377), used
    by readPersistedBuildId when 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.json left by an earlier build is read instead. Reproduced in the
    build output: the client stamped 5d94d82bbcf65710 while the generated handler was configured with the stale
    buildId: "e7399f5b6259c6a7". Per @solidjs/web server.js (the callerBuild !== config.buildId branch), 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

  1. everton-dgn commented on Oct 31, 2026

    @everton-dgn
    Author

    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 buildId match in our build.

    One follow-up, low priority: the client writeBundle now writes .vite/solid-server-functions.json to the directory the client build actually wrote (writtenClientOutDir, dist/esm/index.mjs L2080-2082). Under nitro that is nitro.options.output.publicDir (nitro/dist/vite.mjs L502), 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 Vite manifest.json the 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 nitro compiled hook after the build.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions