fix: fail loudly when client code imports server-only entry points#2207
Merged
Conversation
A query() without "use server" that imports a server-only utility pulls the whole import chain into the client bundle. Server-only code then throws during client module evaluation, killing hydration before the router attaches its handlers - forms silently fall back to a native POST to the placeholder action URL and navigate to a dead page, with no diagnostic pointing at the actual mistake. Add a resolve-time guard that errors in the client environment when a module imports @solidjs/start/http, /middleware, or /config, naming the importing file and the fix. Legitimate "use server" usage is unaffected because the directives compiler strips those imports from client modules before resolution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 42328dd The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
✅ Deploy Preview for solid-start-landing-page ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
commit: |
…a separate plugin Per review feedback, drop the standalone server-only-guard plugin and instead build on the boundary-modules plugin from #2167: the public @solidjs/start/http and @solidjs/start/middleware entries now carry an import "server-only" marker, so pulling them into the client bundle fails at resolve time with an actionable error (#2068). - extract boundary-modules into its own module and extend its errors with fix guidance ("use server" / clientOnly()) - split src/http into a marked public entry re-exporting ./http.ts, so the isomorphic <HttpHeader> (which guards usage behind isServer) can keep importing the helpers without tripping the client-env check - @solidjs/start/config is no longer guarded: it is loaded directly by Node when evaluating vite.config.ts, outside the plugin pipeline, so it cannot carry the marker Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lxsmnsyc
approved these changes
Jul 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2068
What is the current behavior?
A
query()without"use server"that imports a server-only utility (the issue'screateSomeClient()using vinxi's HTTP APIs —@solidjs/start/httpon v2) pulls the entire import chain into the client bundle. Any top-level server-only code then throws during client module evaluation, which kills hydration before the router attaches its handlers — so an unrelatedaction()form on the same page silently falls back to a native POST to the SSR'd placeholderaction="https://action/…"URL and navigates to a dead page. There is no diagnostic anywhere pointing at the actual mistake.What is the new behavior?
Per review feedback, this now builds on #2167's
server-only/client-onlyboundary modules instead of a separate plugin: the public@solidjs/start/httpand@solidjs/start/middlewareentries carry animport "server-only"marker, so pulling them into the client bundle fails at resolve time.https://action/…."use server"functions importing these helpers still work — the directives compiler strips those imports from client modules before resolution ever happens. API routes and middleware resolve in the SSR environment and are untouched. Full playwright e2e suite inapps/testspasses (27 passed, 1 skipped).Implementation notes
server-onlyandclient-onlymodules (#2162) #2167 cherry-picked verbatim (credit @YanAnghelp) — this PR should rebase cleanly whenever that lands. On top of it:"use server"/clientOnly()).src/httpis split: the public entry isimport "server-only"; export * from "./http.ts". This is needed because the isomorphic<HttpHeader>component (exported from the root@solidjs/startentry) importsappendHeader/setHeaderat module scope and only guards usage behindisServer— with the marker on the implementation itself, every client bundle would fail. Internal isomorphic code imports./http.tsdirectly; users go through the marked entry.@solidjs/start/configis no longer guarded (it was in the plugin version): it's loaded directly by Node when evaluatingvite.config.ts, outside the Vite plugin pipeline, so a bareserver-onlyimport there would crash every app withERR_MODULE_NOT_FOUND.@solidjs/start/serveris deliberately not marked: route files legitimately import types from it (e.g.APIHandler).…/@solidjs/start/src/http/index.ts) as the offending client module rather than the user's file that imported it — the import chain isn't available inresolveId. The extended message compensates by stating the fix.🤖 Generated with Claude Code