diff --git a/.changeset/fix-baseurl-api-routes.md b/.changeset/fix-baseurl-api-routes.md new file mode 100644 index 000000000..1f94f9673 --- /dev/null +++ b/.changeset/fix-baseurl-api-routes.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": patch +--- + +Strip baseURL prefix before matching API routes. diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index b8cf5bf58..06d4fbacb 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -73,7 +73,7 @@ export function solidStart(options?: SolidStartOptions): Array { }, }; }, - async config(_, env) { + async config(config, env) { const clientInput = [handlers.client]; if (env.command === "build") { const clientRouter: BaseFileSystemRouter = (globalThis as any).ROUTERS.client; @@ -143,6 +143,9 @@ export function solidStart(options?: SolidStartOptions): Array { "import.meta.env.START_CLIENT_ENTRY": JSON.stringify(handlers.client), "import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay), "import.meta.env.SEROVAL_MODE": JSON.stringify(start.serialization?.mode || "json"), + "import.meta.env.SERVER_BASE_URL": JSON.stringify( + (config as { server?: { baseURL?: string } })?.server?.baseURL ?? "", + ), }, builder: { sharedPlugins: true, diff --git a/packages/start/src/env.d.ts b/packages/start/src/env.d.ts index fad2a39ab..329c0de8d 100644 --- a/packages/start/src/env.d.ts +++ b/packages/start/src/env.d.ts @@ -13,5 +13,5 @@ interface SolidStartMetaEnv { START_CLIENT_ENTRY: string; START_ISLANDS: boolean; // START_DEV_OVERLAY: boolean; - // SERVER_BASE_URL: string; + SERVER_BASE_URL: string; } diff --git a/packages/start/src/server/handler.ts b/packages/start/src/server/handler.ts index 206bc9e26..1a4ff7409 100644 --- a/packages/start/src/server/handler.ts +++ b/packages/start/src/server/handler.ts @@ -10,6 +10,7 @@ import { decorateHandler, decorateMiddleware } from "./fetchEvent.ts"; import { getSsrManifest } from "./manifest/ssr-manifest.ts"; import { matchAPIRoute } from "./routes.ts"; import { handleServerFunction } from "./server-functions-handler.ts"; +import { stripPathBase } from "./strip-path-base.ts"; import type { APIEvent, FetchEvent, HandlerOptions, PageEvent } from "./types.ts"; import { getExpectedRedirectStatus } from "./util.ts"; @@ -254,6 +255,7 @@ function produceResponseWithEventHeaders(res: Response) { } function stripBaseUrl(path: string) { - if (import.meta.env.BASE_URL === "/" || import.meta.env.BASE_URL === "") return path; - return path.slice(import.meta.env.BASE_URL.length); + const base = + import.meta.env.SERVER_BASE_URL || import.meta.env.BASE_URL || "/"; + return stripPathBase(path, base); } diff --git a/packages/start/src/server/strip-path-base.spec.ts b/packages/start/src/server/strip-path-base.spec.ts new file mode 100644 index 000000000..a6e642da4 --- /dev/null +++ b/packages/start/src/server/strip-path-base.spec.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; + +import { stripPathBase } from "./strip-path-base.ts"; + +describe("stripPathBase", () => { + it("leaves path unchanged when base is root or empty", () => { + expect(stripPathBase("/api/hello", "/")).toBe("/api/hello"); + expect(stripPathBase("/api/hello", "")).toBe("/api/hello"); + }); + + it("strips base without trailing slash", () => { + expect(stripPathBase("/myapp/api/hello", "/myapp")).toBe("/api/hello"); + }); + + it("strips base with trailing slash", () => { + expect(stripPathBase("/myapp/api/hello", "/myapp/")).toBe("/api/hello"); + }); + + it("maps exact base match to /", () => { + expect(stripPathBase("/myapp", "/myapp")).toBe("/"); + }); + + it("does not strip when path is not under base", () => { + expect(stripPathBase("/other/api", "/myapp")).toBe("/other/api"); + }); +}); diff --git a/packages/start/src/server/strip-path-base.ts b/packages/start/src/server/strip-path-base.ts new file mode 100644 index 000000000..e15215506 --- /dev/null +++ b/packages/start/src/server/strip-path-base.ts @@ -0,0 +1,7 @@ +/** Strip deploy base path from a URL pathname so API and page routes match file routes. */ +export function stripPathBase(path: string, base: string): string { + if (base === "/" || base === "") return path; + const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base; + if (normalizedBase === "") return path; + return path.startsWith(normalizedBase) ? path.slice(normalizedBase.length) || "/" : path; +}