Skip to content

feat(web): refuse server function calls from another build - #4215

Open
lxsmnsyc wants to merge 1 commit into
solidjs:nextfrom
lxsmnsyc:feat/server-function-skew
Open

lxsmnsyc wants to merge 1 commit into
solidjs:nextfrom
lxsmnsyc:feat/server-function-skew

Conversation

@lxsmnsyc

Copy link
Copy Markdown
Member

The unknown-id 404 (#3110) catches a function that a deploy removed or renamed. It cannot catch one whose id survived the deploy but whose arguments or result changed shape, because ids hash the file path and the name, not the body. This adds an opt-in build id check for that case. It is the core version of solidjs/solid-start#2363.

// client entry
configureServerFunctionsClient({ buildId: BUILD_ID });
// server entry
configureServerFunctionsServer({ buildId: BUILD_ID });

Changes

  • Add BUILD_HEADER (X-Server-Function-Build) to the shared layer. Both entries re-export it.
  • Add buildId to the client config. The transport sends it on every call that is not GET-encoded. GET-encoded reads stay header-free (The server functions implementation adds a X-Server-Function-Instance header, making it impossible to preload them reliably across browsers with <link> tags #3406), so caches and preloads still match them by url.
  • Add buildId to the server config. A call whose header names another build gets 409, with the server's id under the same header and Cache-Control: no-store. The arguments are not parsed and the function does not run.
  • The check runs after the origin gate, unlike the unknown-id 404. Only the client runtime sends the header, and it also sends the fetch metadata the gate reads.
  • Calls without the header are not checked. This covers form posts without the client runtime and clients with no buildId. A server with no buildId checks nothing.
  • Add the header to the preflight's default allowed headers. The CORS answer already exposes every response header, so cross-origin clients can read the label.
  • The client rejects a labelled 409 with versionSkew: true and a message naming version skew. Like unknownFunction, the recovery is left to the integration, which typically reloads the document.
  • Document the option next to the unknown-id paragraph in 10-server-functions.md.

Testing

  • New test/server/server-functions-build-id.spec.tsx (8 tests) runs against the built bundles. It covers the refusal, a matching build, a call without the header, a server without an id, the client error flag, a GET read staying header-free, and a client without an id.
  • server-functions-version-skew.spec.tsx still passes.
  • pnpm test-types passes.
  • The server suite passes except link-claim.spec.tsx and lowercase-on-attribute.spec.tsx. These fail the same way on next without this change in my local setup, which has a stale compiler build. The DOM suite has the same kind of unrelated failures.

🤖 Generated with Claude Code

- Add `buildId` to `configureServerFunctionsClient` and `configureServerFunctionsServer`.
- The client sends `X-Server-Function-Build` on every call that is not GET-encoded.
- A server with a different id answers 409 with its own id and does not run the function.
- The client rejects with `versionSkew: true`, so an integration can reload the page.
- GET-encoded reads stay header-free, so caches and preloads still match them by url.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9b331dc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
test-integration Patch
board-example Patch
server-dbmon-example Patch
todos-server-example Patch
@solidjs/compiler Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codspeed

codspeed Bot commented Oct 22, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 6.39%

2 warnings

⚠️ 4 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 195 untouched benchmarks
⏩ 5 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ memo + sync render effect only (reference) 30 ms 32.1 ms -6.39%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing lxsmnsyc:feat/server-function-skew (9b331dc) with next (5b1b777)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

Copy link
Copy Markdown
Member

Direction from Ryan for this one: keep the server-side check in core, and move the client half out of the transport.

  • Size: as written, the transport change costs every server-function app +354 B minified, even with no buildId, and it fails the Size check on six scenarios, including the frozen base and live server components floors.
  • One skew signal: a build mismatch is the same situation as the unknown-id 404: this client came from another build, so reload. Have the 409 carry the existing unknown-function label (UNKNOWN_HEADER) so the client surfaces it as unknownFunction. Then there's no new client response code and no second versionSkew flag, and integrations check one thing.
  • The header comes from the integration: the vite plugin knows the build id, and it can add the header through prepareRequest. That keeps client bytes at 0, keeps BUILD_HEADER out of the public exports, and leaves room for platform skew protection (Vercel-style deployment pinning by header or cookie) as just another prepareRequest.

The server check itself (after the origin gate, before the body is read, skipped without a header or without a server buildId) looks right, and the CORS change is safe.

— Claude via Claude Code


Generated by Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants