From 1e3dc30f46fd98d479e8c8831ccec02078be414b Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Thu, 16 Jul 2026 21:26:05 -0500 Subject: [PATCH 1/4] fix: fail loudly when client code imports server-only entry points 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 --- .changeset/fix-2068-server-only-guard.md | 5 +++ packages/start/src/config/index.ts | 2 + .../src/config/server-only-guard.spec.ts | 37 +++++++++++++++++++ .../start/src/config/server-only-guard.ts | 35 ++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 .changeset/fix-2068-server-only-guard.md create mode 100644 packages/start/src/config/server-only-guard.spec.ts create mode 100644 packages/start/src/config/server-only-guard.ts diff --git a/.changeset/fix-2068-server-only-guard.md b/.changeset/fix-2068-server-only-guard.md new file mode 100644 index 000000000..7128021f9 --- /dev/null +++ b/.changeset/fix-2068-server-only-guard.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +fix: fail loudly when client-reachable code imports `@solidjs/start`'s server-only entry points (`/http`, `/middleware`, `/config`) instead of silently shipping them to the browser, where they crashed hydration and broke unrelated actions/forms with no diagnostic (#2068) diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index f8a574e9d..95a27e1cf 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -12,6 +12,7 @@ import { fsRoutes } from "./fs-routes/index.ts"; import type { BaseFileSystemRouter } from "./fs-routes/router.ts"; import lazy from "./lazy.ts"; import { manifest } from "./manifest.ts"; +import { serverOnlyGuard } from "./server-only-guard.ts"; import { parseIdQuery } from "./utils.ts"; export interface SolidStartOptions { @@ -209,6 +210,7 @@ export function solidStart(options?: SolidStartOptions): Array { }), lazy(), envPlugin(options?.env), + serverOnlyGuard(), { name: "solid-start:virtual-modules", async resolveId(id) { diff --git a/packages/start/src/config/server-only-guard.spec.ts b/packages/start/src/config/server-only-guard.spec.ts new file mode 100644 index 000000000..d50461b0b --- /dev/null +++ b/packages/start/src/config/server-only-guard.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; + +import { serverOnlyGuard } from "./server-only-guard.ts"; + +function resolveWith(id: string, ssr: boolean) { + const plugin = serverOnlyGuard() as any; + const context = { + error(message: string): never { + throw new Error(message); + }, + }; + return () => plugin.resolveId.call(context, id, "/src/lib/someClient.ts", { ssr }); +} + +describe("serverOnlyGuard", () => { + it("fails when a client module imports @solidjs/start/http", () => { + expect(resolveWith("@solidjs/start/http", false)).toThrowError( + /"@solidjs\/start\/http" is server-only.*someClient\.ts/s, + ); + }); + + it("fails for the other server-only entry points in client modules", () => { + expect(resolveWith("@solidjs/start/middleware", false)).toThrowError(/server-only/); + expect(resolveWith("@solidjs/start/config", false)).toThrowError(/server-only/); + }); + + it("allows server modules to import server-only entry points", () => { + expect(resolveWith("@solidjs/start/http", true)).not.toThrow(); + expect(resolveWith("@solidjs/start/middleware", true)).not.toThrow(); + }); + + it("ignores unrelated ids", () => { + expect(resolveWith("@solidjs/start/client", false)).not.toThrow(); + expect(resolveWith("solid-js", false)).not.toThrow(); + expect(resolveWith("./local-module.ts", false)).not.toThrow(); + }); +}); diff --git a/packages/start/src/config/server-only-guard.ts b/packages/start/src/config/server-only-guard.ts new file mode 100644 index 000000000..9e982054f --- /dev/null +++ b/packages/start/src/config/server-only-guard.ts @@ -0,0 +1,35 @@ +import type { Plugin } from "vite"; + +const SERVER_ONLY_MODULES = new Set([ + "@solidjs/start/http", + "@solidjs/start/middleware", + "@solidjs/start/config", +]); + +/** + * Fails resolution when a client-reachable module imports one of + * `@solidjs/start`'s server-only entry points. + * + * Without this the import chain silently ends up in the client bundle, where + * server-only code throws during module evaluation, killing hydration before + * the router attaches its handlers — forms then fall back to a native POST to + * the placeholder `action` URL and the page dies with no diagnostic pointing + * at the actual mistake (https://github.com/solidjs/solid-start/issues/2068). + */ +export function serverOnlyGuard(): Plugin { + return { + name: "solid-start:server-only-guard", + enforce: "pre", + resolveId(id, importer, { ssr }) { + if (!ssr && SERVER_ONLY_MODULES.has(id)) { + this.error( + `"${id}" is server-only, but it is imported by "${importer ?? "unknown"}", ` + + `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. ` + + `(https://github.com/solidjs/solid-start/issues/2068)`, + ); + } + }, + }; +} From 560e5239696d25ceb24b371c906b66fbc5324258 Mon Sep 17 00:00:00 2001 From: YanAnghelp Date: Fri, 26 Jun 2026 13:04:48 +0800 Subject: [PATCH 2/4] feat: support `server-only` and `client-only` modules (#2162) --- packages/start/env.d.ts | 12 ++++++++++++ packages/start/src/config/index.ts | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/packages/start/env.d.ts b/packages/start/env.d.ts index 2c9155d1c..c638b96cd 100644 --- a/packages/start/env.d.ts +++ b/packages/start/env.d.ts @@ -7,3 +7,15 @@ declare namespace App { [key: string | symbol]: any; } } + +/** + * Import `server-only` to ensure this module is never bundled for the client. + * Importing it in a client module will throw a build error. + */ +declare module "server-only" {} + +/** + * Import `client-only` to ensure this module is never bundled for the server. + * Importing it in a server module will throw a build error. + */ +declare module "client-only" {} diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 95a27e1cf..d13a8b10e 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -211,6 +211,29 @@ export function solidStart(options?: SolidStartOptions): Array { lazy(), envPlugin(options?.env), serverOnlyGuard(), + { + name: "solid-start:boundary-modules", + enforce: "pre", + resolveId(id, importer, { ssr }) { + if (id === "server-only") { + if (!ssr) + this.error( + `Attempt to import 'server-only' in a client module: ${importer}`, + ); + } else if (id === "client-only") { + if (ssr) + this.error( + `Attempt to import 'client-only' in a server module: ${importer}`, + ); + } else { + return null; + } + return "\0solid-start:boundary-modules:id"; + }, + load(id) { + if (id === "\0solid-start:boundary-modules:id") return "export {}"; + }, + }, { name: "solid-start:virtual-modules", async resolveId(id) { From 9451bce3ca32d9f98d41d7d6118e250c8ee70bcf Mon Sep 17 00:00:00 2001 From: YanAnghelp Date: Fri, 26 Jun 2026 16:58:58 +0800 Subject: [PATCH 3/4] chore: add changeset --- .changeset/support-server-client-only.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/support-server-client-only.md diff --git a/.changeset/support-server-client-only.md b/.changeset/support-server-client-only.md new file mode 100644 index 000000000..935fe31ee --- /dev/null +++ b/.changeset/support-server-client-only.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": minor +--- + +Add support for `server-only` and `client-only` modules From 99a5511617a908a673b5df7fb11176b873ec8a50 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Fri, 17 Jul 2026 15:20:58 -0500 Subject: [PATCH 4/4] refactor: mark server-only entries via server-only import instead of 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 (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 --- .changeset/fix-2068-server-only-guard.md | 2 +- .../start/src/config/boundary-modules.spec.ts | 50 ++++ packages/start/src/config/boundary-modules.ts | 45 ++++ packages/start/src/config/index.ts | 27 +-- .../src/config/server-only-guard.spec.ts | 37 --- .../start/src/config/server-only-guard.ts | 35 --- packages/start/src/http/http.ts | 216 +++++++++++++++++ packages/start/src/http/index.ts | 221 +----------------- packages/start/src/middleware/index.ts | 2 + packages/start/src/shared/HttpHeader.tsx | 2 +- 10 files changed, 323 insertions(+), 314 deletions(-) create mode 100644 packages/start/src/config/boundary-modules.spec.ts create mode 100644 packages/start/src/config/boundary-modules.ts delete mode 100644 packages/start/src/config/server-only-guard.spec.ts delete mode 100644 packages/start/src/config/server-only-guard.ts create mode 100644 packages/start/src/http/http.ts diff --git a/.changeset/fix-2068-server-only-guard.md b/.changeset/fix-2068-server-only-guard.md index 7128021f9..895268b8c 100644 --- a/.changeset/fix-2068-server-only-guard.md +++ b/.changeset/fix-2068-server-only-guard.md @@ -2,4 +2,4 @@ "@solidjs/start": patch --- -fix: fail loudly when client-reachable code imports `@solidjs/start`'s server-only entry points (`/http`, `/middleware`, `/config`) instead of silently shipping them to the browser, where they crashed hydration and broke unrelated actions/forms with no diagnostic (#2068) +fix: mark `@solidjs/start/http` and `@solidjs/start/middleware` as `server-only` so importing them from client-reachable code fails loudly at dev/build time, instead of silently shipping them to the browser where they crashed hydration and broke unrelated actions/forms with no diagnostic (#2068) diff --git a/packages/start/src/config/boundary-modules.spec.ts b/packages/start/src/config/boundary-modules.spec.ts new file mode 100644 index 000000000..830eb99a9 --- /dev/null +++ b/packages/start/src/config/boundary-modules.spec.ts @@ -0,0 +1,50 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +import { boundaryModules } from "./boundary-modules.ts"; + +function resolveWith(id: string, ssr: boolean) { + const plugin = boundaryModules() as any; + const context = { + error(message: string): never { + throw new Error(message); + }, + }; + return () => plugin.resolveId.call(context, id, "/src/lib/someClient.ts", { ssr }); +} + +describe("boundaryModules", () => { + it("fails when a client module imports server-only", () => { + expect(resolveWith("server-only", false)).toThrowError(/server-only.*someClient\.ts/s); + }); + + it("fails when a server module imports client-only", () => { + expect(resolveWith("client-only", true)).toThrowError(/client-only.*someClient\.ts/s); + }); + + it("resolves the markers to an empty module in the allowed environment", () => { + const plugin = boundaryModules() as any; + const serverOnlyId = resolveWith("server-only", true)(); + const clientOnlyId = resolveWith("client-only", false)(); + expect(plugin.load(serverOnlyId)).toBe("export {}"); + expect(plugin.load(clientOnlyId)).toBe("export {}"); + }); + + it("ignores unrelated ids", () => { + expect(resolveWith("solid-js", false)()).toBe(null); + expect(resolveWith("./local-module.ts", false)()).toBe(null); + }); + + // Regression guard for #2068: Start's server-only entry points must carry + // the marker so client-reachable imports of them fail loudly instead of + // shipping server code to the browser and crashing hydration. + it("marks Start's server-only entry points with server-only", () => { + for (const entry of ["http/index.ts", "middleware/index.ts"]) { + const source = readFileSync(join(import.meta.dirname, "..", entry), "utf-8"); + expect(source, `src/${entry} must import "server-only"`).toMatch( + /^import "server-only";$/m, + ); + } + }); +}); diff --git a/packages/start/src/config/boundary-modules.ts b/packages/start/src/config/boundary-modules.ts new file mode 100644 index 000000000..2325c2e1f --- /dev/null +++ b/packages/start/src/config/boundary-modules.ts @@ -0,0 +1,45 @@ +import type { Plugin } from "vite"; + +const VIRTUAL_ID = "\0solid-start:boundary-modules:id"; + +/** + * Supports `server-only` and `client-only` marker modules (#2162): importing + * `server-only` from a client module (or `client-only` from a server module) + * fails at resolve time; in the allowed environment the marker resolves to an + * empty module. + * + * Start's own server-only entry points (`@solidjs/start/http`, + * `@solidjs/start/middleware`) import `server-only` themselves, so pulling + * them into the client bundle fails loudly instead of shipping server code to + * the browser, where it crashed hydration and broke unrelated actions/forms + * with no diagnostic (https://github.com/solidjs/solid-start/issues/2068). + */ +export function boundaryModules(): Plugin { + return { + name: "solid-start:boundary-modules", + enforce: "pre", + resolveId(id, importer, { ssr }) { + if (id === "server-only") { + if (!ssr) + this.error( + `Attempt to import 'server-only' in a client module: ${importer}. ` + + `Code that uses this module must run only on the server: mark it with ` + + `"use server", or make sure it is only imported by server code.`, + ); + } else if (id === "client-only") { + if (ssr) + this.error( + `Attempt to import 'client-only' in a server module: ${importer}. ` + + `Code that uses this module must run only in the browser: make sure it ` + + `is only imported by client code (e.g. wrap components with clientOnly()).`, + ); + } else { + return null; + } + return VIRTUAL_ID; + }, + load(id) { + if (id === VIRTUAL_ID) return "export {}"; + }, + }; +} diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index d13a8b10e..41ab17daa 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -4,6 +4,7 @@ import { extname, isAbsolute, join } from "node:path"; import type { PluginOption } from "vite"; import solid, { type Options as SolidOptions } from "vite-plugin-solid"; import { type ServerFunctionsOptions, serverFunctionsPlugin } from "../directives/index.ts"; +import { boundaryModules } from "./boundary-modules.ts"; import { DEFAULT_EXTENSIONS, VIRTUAL_MODULES, VITE_ENVIRONMENTS } from "./constants.ts"; import { devServer } from "./dev-server.ts"; import { envPlugin, type EnvPluginOptions } from "./env.ts"; @@ -12,7 +13,6 @@ import { fsRoutes } from "./fs-routes/index.ts"; import type { BaseFileSystemRouter } from "./fs-routes/router.ts"; import lazy from "./lazy.ts"; import { manifest } from "./manifest.ts"; -import { serverOnlyGuard } from "./server-only-guard.ts"; import { parseIdQuery } from "./utils.ts"; export interface SolidStartOptions { @@ -210,30 +210,7 @@ export function solidStart(options?: SolidStartOptions): Array { }), lazy(), envPlugin(options?.env), - serverOnlyGuard(), - { - name: "solid-start:boundary-modules", - enforce: "pre", - resolveId(id, importer, { ssr }) { - if (id === "server-only") { - if (!ssr) - this.error( - `Attempt to import 'server-only' in a client module: ${importer}`, - ); - } else if (id === "client-only") { - if (ssr) - this.error( - `Attempt to import 'client-only' in a server module: ${importer}`, - ); - } else { - return null; - } - return "\0solid-start:boundary-modules:id"; - }, - load(id) { - if (id === "\0solid-start:boundary-modules:id") return "export {}"; - }, - }, + boundaryModules(), { name: "solid-start:virtual-modules", async resolveId(id) { diff --git a/packages/start/src/config/server-only-guard.spec.ts b/packages/start/src/config/server-only-guard.spec.ts deleted file mode 100644 index d50461b0b..000000000 --- a/packages/start/src/config/server-only-guard.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { serverOnlyGuard } from "./server-only-guard.ts"; - -function resolveWith(id: string, ssr: boolean) { - const plugin = serverOnlyGuard() as any; - const context = { - error(message: string): never { - throw new Error(message); - }, - }; - return () => plugin.resolveId.call(context, id, "/src/lib/someClient.ts", { ssr }); -} - -describe("serverOnlyGuard", () => { - it("fails when a client module imports @solidjs/start/http", () => { - expect(resolveWith("@solidjs/start/http", false)).toThrowError( - /"@solidjs\/start\/http" is server-only.*someClient\.ts/s, - ); - }); - - it("fails for the other server-only entry points in client modules", () => { - expect(resolveWith("@solidjs/start/middleware", false)).toThrowError(/server-only/); - expect(resolveWith("@solidjs/start/config", false)).toThrowError(/server-only/); - }); - - it("allows server modules to import server-only entry points", () => { - expect(resolveWith("@solidjs/start/http", true)).not.toThrow(); - expect(resolveWith("@solidjs/start/middleware", true)).not.toThrow(); - }); - - it("ignores unrelated ids", () => { - expect(resolveWith("@solidjs/start/client", false)).not.toThrow(); - expect(resolveWith("solid-js", false)).not.toThrow(); - expect(resolveWith("./local-module.ts", false)).not.toThrow(); - }); -}); diff --git a/packages/start/src/config/server-only-guard.ts b/packages/start/src/config/server-only-guard.ts deleted file mode 100644 index 9e982054f..000000000 --- a/packages/start/src/config/server-only-guard.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Plugin } from "vite"; - -const SERVER_ONLY_MODULES = new Set([ - "@solidjs/start/http", - "@solidjs/start/middleware", - "@solidjs/start/config", -]); - -/** - * Fails resolution when a client-reachable module imports one of - * `@solidjs/start`'s server-only entry points. - * - * Without this the import chain silently ends up in the client bundle, where - * server-only code throws during module evaluation, killing hydration before - * the router attaches its handlers — forms then fall back to a native POST to - * the placeholder `action` URL and the page dies with no diagnostic pointing - * at the actual mistake (https://github.com/solidjs/solid-start/issues/2068). - */ -export function serverOnlyGuard(): Plugin { - return { - name: "solid-start:server-only-guard", - enforce: "pre", - resolveId(id, importer, { ssr }) { - if (!ssr && SERVER_ONLY_MODULES.has(id)) { - this.error( - `"${id}" is server-only, but it is imported by "${importer ?? "unknown"}", ` + - `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. ` + - `(https://github.com/solidjs/solid-start/issues/2068)`, - ); - } - }, - }; -} diff --git a/packages/start/src/http/http.ts b/packages/start/src/http/http.ts new file mode 100644 index 000000000..0b4339929 --- /dev/null +++ b/packages/start/src/http/http.ts @@ -0,0 +1,216 @@ +import type { H3Event, HTTPEvent, InferEventInput } from "h3"; +import * as h3 from "h3"; +import { getRequestEvent } from "solid-js/web"; + +function _setContext(event: H3Event, key: string, value: any) { + event.context[key] = value; +} + +function _getContext(event: H3Event, key: string) { + return event.context[key]; +} + +function getEvent() { + return getRequestEvent()!.nativeEvent; +} + +export function getWebRequest(): Request { + return getEvent().req; +} + +export const HTTPEventSymbol = Symbol("$HTTPEvent"); + +export function isEvent(obj: any): obj is h3.H3Event | { [HTTPEventSymbol]: h3.H3Event } { + return ( + typeof obj === "object" && + (obj instanceof h3.H3Event || + obj?.[HTTPEventSymbol] instanceof h3.H3Event || + obj?.__is_event__ === true) + ); + // Implement logic to check if obj is an H3Event +} + +type Tail = T extends [any, ...infer U] ? U : never; + +type PrependOverload< + TOriginal extends (...args: Array) => any, + TOverload extends (...args: Array) => any, +> = TOverload & TOriginal; + +// add an overload to the function without the event argument +type WrapFunction) => any> = PrependOverload< + TFn, + ( + ...args: Parameters extends [h3.HTTPEvent | h3.H3Event, ...infer TArgs] + ? TArgs + : Parameters + ) => ReturnType +>; + +function createWrapperFunction) => any>( + h3Function: TFn, +): WrapFunction { + return ((...args: Array) => { + const event = args[0]; + if (!isEvent(event)) { + args.unshift(getEvent()); + } else { + args[0] = + event instanceof h3.H3Event || (event as any).__is_event__ ? event : event[HTTPEventSymbol]; + } + + return (h3Function as any)(...args); + }) as any; +} + +// Creating wrappers for each utility and exporting them with their original names +// readRawBody => getWebRequest().text()/.arrayBuffer() +type WrappedReadBody = >( + ...args: Tail>> +) => ReturnType>; +export const readBody = createWrapperFunction(h3.readBody) as PrependOverload< + typeof h3.readBody, + WrappedReadBody +>; +type WrappedGetQuery = , undefined>>( + ...args: Tail>> +) => ReturnType>; +export const getQuery = createWrapperFunction(h3.getQuery) as PrependOverload< + typeof h3.getQuery, + WrappedGetQuery +>; +export const isMethod = createWrapperFunction(h3.isMethod); +export const isPreflightRequest = createWrapperFunction(h3.isPreflightRequest); +type WrappedGetValidatedQuery = < + T extends HTTPEvent, + TEventInput = InferEventInput<"query", H3Event, T>, +>( + ...args: Tail>> +) => ReturnType>; +export const getValidatedQuery = createWrapperFunction(h3.getValidatedQuery) as PrependOverload< + typeof h3.getValidatedQuery, + WrappedGetValidatedQuery +>; +export const getRouterParams = createWrapperFunction(h3.getRouterParams); +export const getRouterParam = createWrapperFunction(h3.getRouterParam); +type WrappedGetValidatedRouterParams = < + T extends HTTPEvent, + TEventInput = InferEventInput<"routerParams", H3Event, T>, +>( + ...args: Tail>> +) => ReturnType>; +export const getValidatedRouterParams = createWrapperFunction( + h3.getValidatedRouterParams, +) as PrependOverload; +export const assertMethod = createWrapperFunction(h3.assertMethod); +export const getRequestHeaders = createWrapperFunction(h3.getRequestHeaders); +export const getRequestHeader = createWrapperFunction(h3.getRequestHeader); +export const getRequestURL = createWrapperFunction(h3.getRequestURL); +export const getRequestHost = createWrapperFunction(h3.getRequestHost); +export const getRequestProtocol = createWrapperFunction(h3.getRequestProtocol); +export const getRequestIP = createWrapperFunction(h3.getRequestIP); +export const setResponseStatus = (code?: number, text?: string) => { + const e = getEvent(); + + if (e.res.status !== undefined) e.res.status = code; + if (e.res.statusText !== undefined) e.res.statusText = text; +}; +export const getResponseStatus = () => getEvent().res.status; +export const getResponseStatusText = () => getEvent().res.statusText; +export const getResponseHeaders = () => Object.fromEntries(getEvent().res.headers.entries()); +export const getResponseHeader = (name: string) => getEvent().res.headers.get(name); +export const setResponseHeaders = (values: Record) => { + const headers = getEvent().res.headers; + for (const [name, value] of Object.entries(values)) { + headers.set(name, value); + } +}; +export const setResponseHeader = (name: string, value: string | string[]) => { + const headers = getEvent().res.headers; + + (Array.isArray(value) ? value : [value]).forEach(value => { + headers.set(name, value); + }); +}; +export const appendResponseHeaders = (values: Record) => { + const headers = getEvent().res.headers; + for (const [name, value] of Object.entries(values)) { + headers.append(name, value); + } +}; +export const appendResponseHeader = (name: string, value: string | string[]) => { + const headers = getEvent().res.headers; + + (Array.isArray(value) ? value : [value]).forEach(value => { + headers.append(name, value); + }); +}; +export const defaultContentType = (type: string) => + getEvent().res.headers.set("content-type", type); +export const proxyRequest = createWrapperFunction(h3.proxyRequest); +export const fetchWithEvent = createWrapperFunction(h3.fetchWithEvent); +export const getProxyRequestHeaders = createWrapperFunction(h3.getProxyRequestHeaders); + +export const parseCookies = createWrapperFunction(h3.parseCookies); +export const getCookie = createWrapperFunction(h3.getCookie); +export const setCookie = createWrapperFunction(h3.setCookie); +export const deleteCookie = createWrapperFunction(h3.deleteCookie); +// not exported :( +type SessionDataT = Record; +type WrappedUseSession = ( + ...args: Tail>> +) => ReturnType>; +export const useSession: WrappedUseSession = createWrapperFunction(h3.useSession); +type WrappedGetSession = ( + ...args: Tail>> +) => ReturnType>; +export const getSession: WrappedGetSession = createWrapperFunction(h3.getSession); +type WrappedUpdateSession = ( + ...args: Tail>> +) => ReturnType>; +export const updateSession: WrappedUpdateSession = createWrapperFunction(h3.updateSession); +type WrappedSealSession = ( + ...args: Tail>> +) => ReturnType>; +export const sealSession: WrappedSealSession = createWrapperFunction(h3.sealSession); +export const unsealSession = createWrapperFunction(h3.unsealSession); +export const clearSession = createWrapperFunction(h3.clearSession); +export const handleCacheHeaders = createWrapperFunction(h3.handleCacheHeaders); +export const handleCors = createWrapperFunction(h3.handleCors); +export const appendCorsHeaders = createWrapperFunction(h3.appendCorsHeaders); +export const appendCorsPreflightHeaders = createWrapperFunction(h3.appendCorsPreflightHeaders); +export const appendHeader = appendResponseHeader; +export const appendHeaders = appendResponseHeaders; +export const setHeader = setResponseHeader; +export const setHeaders = setResponseHeaders; +export const getHeader = getRequestHeader; +export const getHeaders = getRequestHeaders; +export const getRequestFingerprint = createWrapperFunction(h3.getRequestFingerprint); +export const getRequestWebStream = () => getEvent().req.body; +export const readFormData = () => getEvent().req.formData(); +type WrappedReadValidatedBody = < + T extends HTTPEvent, + TEventInput = InferEventInput<"body", H3Event, T>, +>( + ...args: Tail>> +) => ReturnType>; +export const readValidatedBody = createWrapperFunction(h3.readValidatedBody) as PrependOverload< + typeof h3.readValidatedBody, + WrappedReadValidatedBody +>; +export const getContext = createWrapperFunction(_getContext); +export const setContext = createWrapperFunction(_setContext); +export const removeResponseHeader = (name: string) => getEvent().res.headers.delete(name); +export const clearResponseHeaders = (headerNames?: string[]) => { + const headers = getEvent().res.headers; + + if (headerNames && headerNames.length > 0) { + for (const name of headerNames) { + headers.delete(name); + } + } else { + for (const name of headers.keys()) { + headers.delete(name); + } + } +}; diff --git a/packages/start/src/http/index.ts b/packages/start/src/http/index.ts index 0b4339929..e1aaa1e15 100644 --- a/packages/start/src/http/index.ts +++ b/packages/start/src/http/index.ts @@ -1,216 +1,7 @@ -import type { H3Event, HTTPEvent, InferEventInput } from "h3"; -import * as h3 from "h3"; -import { getRequestEvent } from "solid-js/web"; +// The public `@solidjs/start/http` entry is server-only: importing it from +// client-reachable code fails at resolve time (#2068). Internal isomorphic +// code (e.g. ``) imports `./http.ts` directly, since it only +// touches these helpers behind an `isServer` check. +import "server-only"; -function _setContext(event: H3Event, key: string, value: any) { - event.context[key] = value; -} - -function _getContext(event: H3Event, key: string) { - return event.context[key]; -} - -function getEvent() { - return getRequestEvent()!.nativeEvent; -} - -export function getWebRequest(): Request { - return getEvent().req; -} - -export const HTTPEventSymbol = Symbol("$HTTPEvent"); - -export function isEvent(obj: any): obj is h3.H3Event | { [HTTPEventSymbol]: h3.H3Event } { - return ( - typeof obj === "object" && - (obj instanceof h3.H3Event || - obj?.[HTTPEventSymbol] instanceof h3.H3Event || - obj?.__is_event__ === true) - ); - // Implement logic to check if obj is an H3Event -} - -type Tail = T extends [any, ...infer U] ? U : never; - -type PrependOverload< - TOriginal extends (...args: Array) => any, - TOverload extends (...args: Array) => any, -> = TOverload & TOriginal; - -// add an overload to the function without the event argument -type WrapFunction) => any> = PrependOverload< - TFn, - ( - ...args: Parameters extends [h3.HTTPEvent | h3.H3Event, ...infer TArgs] - ? TArgs - : Parameters - ) => ReturnType ->; - -function createWrapperFunction) => any>( - h3Function: TFn, -): WrapFunction { - return ((...args: Array) => { - const event = args[0]; - if (!isEvent(event)) { - args.unshift(getEvent()); - } else { - args[0] = - event instanceof h3.H3Event || (event as any).__is_event__ ? event : event[HTTPEventSymbol]; - } - - return (h3Function as any)(...args); - }) as any; -} - -// Creating wrappers for each utility and exporting them with their original names -// readRawBody => getWebRequest().text()/.arrayBuffer() -type WrappedReadBody = >( - ...args: Tail>> -) => ReturnType>; -export const readBody = createWrapperFunction(h3.readBody) as PrependOverload< - typeof h3.readBody, - WrappedReadBody ->; -type WrappedGetQuery = , undefined>>( - ...args: Tail>> -) => ReturnType>; -export const getQuery = createWrapperFunction(h3.getQuery) as PrependOverload< - typeof h3.getQuery, - WrappedGetQuery ->; -export const isMethod = createWrapperFunction(h3.isMethod); -export const isPreflightRequest = createWrapperFunction(h3.isPreflightRequest); -type WrappedGetValidatedQuery = < - T extends HTTPEvent, - TEventInput = InferEventInput<"query", H3Event, T>, ->( - ...args: Tail>> -) => ReturnType>; -export const getValidatedQuery = createWrapperFunction(h3.getValidatedQuery) as PrependOverload< - typeof h3.getValidatedQuery, - WrappedGetValidatedQuery ->; -export const getRouterParams = createWrapperFunction(h3.getRouterParams); -export const getRouterParam = createWrapperFunction(h3.getRouterParam); -type WrappedGetValidatedRouterParams = < - T extends HTTPEvent, - TEventInput = InferEventInput<"routerParams", H3Event, T>, ->( - ...args: Tail>> -) => ReturnType>; -export const getValidatedRouterParams = createWrapperFunction( - h3.getValidatedRouterParams, -) as PrependOverload; -export const assertMethod = createWrapperFunction(h3.assertMethod); -export const getRequestHeaders = createWrapperFunction(h3.getRequestHeaders); -export const getRequestHeader = createWrapperFunction(h3.getRequestHeader); -export const getRequestURL = createWrapperFunction(h3.getRequestURL); -export const getRequestHost = createWrapperFunction(h3.getRequestHost); -export const getRequestProtocol = createWrapperFunction(h3.getRequestProtocol); -export const getRequestIP = createWrapperFunction(h3.getRequestIP); -export const setResponseStatus = (code?: number, text?: string) => { - const e = getEvent(); - - if (e.res.status !== undefined) e.res.status = code; - if (e.res.statusText !== undefined) e.res.statusText = text; -}; -export const getResponseStatus = () => getEvent().res.status; -export const getResponseStatusText = () => getEvent().res.statusText; -export const getResponseHeaders = () => Object.fromEntries(getEvent().res.headers.entries()); -export const getResponseHeader = (name: string) => getEvent().res.headers.get(name); -export const setResponseHeaders = (values: Record) => { - const headers = getEvent().res.headers; - for (const [name, value] of Object.entries(values)) { - headers.set(name, value); - } -}; -export const setResponseHeader = (name: string, value: string | string[]) => { - const headers = getEvent().res.headers; - - (Array.isArray(value) ? value : [value]).forEach(value => { - headers.set(name, value); - }); -}; -export const appendResponseHeaders = (values: Record) => { - const headers = getEvent().res.headers; - for (const [name, value] of Object.entries(values)) { - headers.append(name, value); - } -}; -export const appendResponseHeader = (name: string, value: string | string[]) => { - const headers = getEvent().res.headers; - - (Array.isArray(value) ? value : [value]).forEach(value => { - headers.append(name, value); - }); -}; -export const defaultContentType = (type: string) => - getEvent().res.headers.set("content-type", type); -export const proxyRequest = createWrapperFunction(h3.proxyRequest); -export const fetchWithEvent = createWrapperFunction(h3.fetchWithEvent); -export const getProxyRequestHeaders = createWrapperFunction(h3.getProxyRequestHeaders); - -export const parseCookies = createWrapperFunction(h3.parseCookies); -export const getCookie = createWrapperFunction(h3.getCookie); -export const setCookie = createWrapperFunction(h3.setCookie); -export const deleteCookie = createWrapperFunction(h3.deleteCookie); -// not exported :( -type SessionDataT = Record; -type WrappedUseSession = ( - ...args: Tail>> -) => ReturnType>; -export const useSession: WrappedUseSession = createWrapperFunction(h3.useSession); -type WrappedGetSession = ( - ...args: Tail>> -) => ReturnType>; -export const getSession: WrappedGetSession = createWrapperFunction(h3.getSession); -type WrappedUpdateSession = ( - ...args: Tail>> -) => ReturnType>; -export const updateSession: WrappedUpdateSession = createWrapperFunction(h3.updateSession); -type WrappedSealSession = ( - ...args: Tail>> -) => ReturnType>; -export const sealSession: WrappedSealSession = createWrapperFunction(h3.sealSession); -export const unsealSession = createWrapperFunction(h3.unsealSession); -export const clearSession = createWrapperFunction(h3.clearSession); -export const handleCacheHeaders = createWrapperFunction(h3.handleCacheHeaders); -export const handleCors = createWrapperFunction(h3.handleCors); -export const appendCorsHeaders = createWrapperFunction(h3.appendCorsHeaders); -export const appendCorsPreflightHeaders = createWrapperFunction(h3.appendCorsPreflightHeaders); -export const appendHeader = appendResponseHeader; -export const appendHeaders = appendResponseHeaders; -export const setHeader = setResponseHeader; -export const setHeaders = setResponseHeaders; -export const getHeader = getRequestHeader; -export const getHeaders = getRequestHeaders; -export const getRequestFingerprint = createWrapperFunction(h3.getRequestFingerprint); -export const getRequestWebStream = () => getEvent().req.body; -export const readFormData = () => getEvent().req.formData(); -type WrappedReadValidatedBody = < - T extends HTTPEvent, - TEventInput = InferEventInput<"body", H3Event, T>, ->( - ...args: Tail>> -) => ReturnType>; -export const readValidatedBody = createWrapperFunction(h3.readValidatedBody) as PrependOverload< - typeof h3.readValidatedBody, - WrappedReadValidatedBody ->; -export const getContext = createWrapperFunction(_getContext); -export const setContext = createWrapperFunction(_setContext); -export const removeResponseHeader = (name: string) => getEvent().res.headers.delete(name); -export const clearResponseHeaders = (headerNames?: string[]) => { - const headers = getEvent().res.headers; - - if (headerNames && headerNames.length > 0) { - for (const name of headerNames) { - headers.delete(name); - } - } else { - for (const name of headers.keys()) { - headers.delete(name); - } - } -}; +export * from "./http.ts"; diff --git a/packages/start/src/middleware/index.ts b/packages/start/src/middleware/index.ts index 19f068274..6ef93f7ea 100644 --- a/packages/start/src/middleware/index.ts +++ b/packages/start/src/middleware/index.ts @@ -1,5 +1,7 @@ // @refresh skip +import "server-only"; + import type { H3Event, Middleware } from "h3"; import { getFetchEvent } from "../server/fetchEvent.ts"; import type { FetchEvent } from "../server/types.ts"; diff --git a/packages/start/src/shared/HttpHeader.tsx b/packages/start/src/shared/HttpHeader.tsx index 6dcb504b6..d69401676 100644 --- a/packages/start/src/shared/HttpHeader.tsx +++ b/packages/start/src/shared/HttpHeader.tsx @@ -3,7 +3,7 @@ import { onCleanup } from "solid-js"; import { getRequestEvent, isServer } from "solid-js/web"; import type { PageEvent } from "../server/types.ts"; -import { appendHeader, setHeader } from "../http/index.ts"; +import { appendHeader, setHeader } from "../http/http.ts"; export interface HttpHeaderProps { name: string;