Skip to content

fix: fail loudly when client code imports server-only entry points#2207

Open
brenelz wants to merge 1 commit into
mainfrom
fix/server-only-import-guard
Open

fix: fail loudly when client code imports server-only entry points#2207
brenelz wants to merge 1 commit into
mainfrom
fix/server-only-import-guard

Conversation

@brenelz

@brenelz brenelz commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #2068

What is the current behavior?

A query() without "use server" that imports a server-only utility (the issue's createSomeClient() using vinxi's HTTP APIs — @solidjs/start/http on 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 unrelated action() form on the same page silently falls back to a native POST to the SSR'd placeholder action="https://action/…" URL and navigates to a dead page. There is no diagnostic anywhere pointing at the actual mistake.

Reproduced on 2.0.0-beta.7 with a mirror of the reporter's repro: hydration dies with process is not defined, playwright captures the native POST https://action/performRandomActionchrome-error://chromewebdata/. Adding "use server" to the unused query fixes everything (causation control). The reporter's exact process.env variant is masked in prod builds by Vite's define, but any real server-only import (e.g. node:async_hooks) reproduces in prod too.

What is the new behavior?

A resolve-time guard (solid-start:server-only-guard) errors in the client environment when a module imports @solidjs/start/http, @solidjs/start/middleware, or @solidjs/start/config, naming the importing file and the fix:

"@solidjs/start/http" is server-only, but it is imported by "src/lib/someClient.ts", which is included in the client bundle. Code that uses it must run only on the server: mark the function or module that uses it with "use server", or move it into a module that is only imported by server code.

  • Dev: error overlay + terminal error on request, instead of a broken page.
  • Prod: hard build failure, instead of shipping a bundle whose forms navigate to https://action/….
  • Legitimate usage unaffected: "use server" functions importing these helpers still work — the directives compiler strips those imports from client modules before resolution ever happens (verified: SSR renders the server-read header, client transform contains no trace of the util). API routes and middleware resolve in the SSR environment and are untouched.

Other information

  • Same resolveId(id, importer, { ssr }) pattern as feat: support server-only and client-only modules (#2162) #2167's opt-in server-only/client-only markers — this PR effectively makes Start's own server-only entry points implicitly server-only. Happy to rebase onto feat: support server-only and client-only modules (#2162) #2167 if it lands first.
  • @solidjs/start/server is deliberately not guarded: route files legitimately import types from it (e.g. APIHandler), and type-only imports aren't always distinguishable at resolve time from the raw specifier.
  • Unit tests in server-only-guard.spec.ts; full playwright e2e suite in apps/tests passes (27 passed, 1 skipped).

🤖 Generated with Claude Code

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-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e3dc30

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

This PR includes changesets to release 1 package
Name Type
@solidjs/start 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

@netlify

netlify Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploy Preview for solid-start-landing-page ready!

Name Link
🔨 Latest commit 1e3dc30
🔍 Latest deploy log https://app.netlify.com/projects/solid-start-landing-page/deploys/6a5992d6dca56700087ee4d3
😎 Deploy Preview https://deploy-preview-2207--solid-start-landing-page.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@pkg-pr-new

pkg-pr-new Bot commented Jul 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@solidjs/start@2207

commit: 1e3dc30

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.

[Bug?]: Server-only imports in unused query silently breaks unrelated action() form

1 participant