From dcfe7596bb5725eafda57c9a358f80eb09b0a004 Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Fri, 13 Mar 2026 19:23:15 +0100 Subject: [PATCH 1/3] fix(vite): handle dotted Nitro routes under baseURL in dev --- src/build/vite/dev.ts | 16 ++++++- .../api/proxy/[...param].ts | 1 + .../baseurl-dotted-param-fixture/index.html | 11 +++++ .../tsconfig.json | 3 ++ .../vite.config.ts | 13 ++++++ test/vite/baseurl-dotted-param.test.ts | 43 +++++++++++++++++++ 6 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 test/vite/baseurl-dotted-param-fixture/api/proxy/[...param].ts create mode 100644 test/vite/baseurl-dotted-param-fixture/index.html create mode 100644 test/vite/baseurl-dotted-param-fixture/tsconfig.json create mode 100644 test/vite/baseurl-dotted-param-fixture/vite.config.ts create mode 100644 test/vite/baseurl-dotted-param.test.ts diff --git a/src/build/vite/dev.ts b/src/build/vite/dev.ts index 04d1f0fd89..894cf10c11 100644 --- a/src/build/vite/dev.ts +++ b/src/build/vite/dev.ts @@ -11,6 +11,7 @@ import { watch as chokidarWatch } from "chokidar"; import { watch as fsWatch } from "node:fs"; import { join } from "pathe"; import { debounce } from "perfect-debounce"; +import { withBase } from "ufo"; import { scanHandlers } from "../../scan.ts"; import { getEnvRunner } from "./env.ts"; @@ -198,6 +199,8 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi return next(); } nodeReq._nitroHandled = true; + const originalURL = nodeReq.url; + nodeReq.url = withBase(nodeReq.url, nitro.options.baseURL); try { // Create web API compat request const req = new NodeRequest({ req: nodeReq, res: nodeRes }); @@ -219,6 +222,8 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi return await sendNodeResponse(nodeRes, envRes); } catch (error) { return next(error); + } finally { + nodeReq.url = originalURL; } }; @@ -226,12 +231,19 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi // https://github.com/vitejs/vite/pull/20866 server.middlewares.use(function nitroDevMiddlewarePre(req, res, next) { const fetchDest = req.headers["sec-fetch-dest"]; + const ext = req.url!.match(/\.([a-z0-9]+)(?:[?#]|$)/i)?.[1]; + const isNitroRoute = ext + ? !!nitro.routing.routes.match( + req.method || "", + new URL(withBase(req.url!, nitro.options.baseURL), "http://localhost").pathname + ) + : false; res.setHeader("vary", "sec-fetch-dest"); if ( // Originating from browser tab or no fetch dest (curl, fetch, etc) and (not script, style, image, etc) (!fetchDest || /^(document|iframe|frame|empty)$/.test(fetchDest)) && - // No file extension (not /src/index.ts) - !req.url!.match(/\.([a-z0-9]+)(?:[?#]|$)/i)?.[1] && + // No file extension (not /src/index.ts) unless it is an explicit Nitro route + (!ext || isNitroRoute) && // Special prefixes (/__vue-router/auto-routes, /@vite-plugin-layouts/, etc) !/^\/(?:__|@)/.test(req.url!) ) { diff --git a/test/vite/baseurl-dotted-param-fixture/api/proxy/[...param].ts b/test/vite/baseurl-dotted-param-fixture/api/proxy/[...param].ts new file mode 100644 index 0000000000..98672e7806 --- /dev/null +++ b/test/vite/baseurl-dotted-param-fixture/api/proxy/[...param].ts @@ -0,0 +1 @@ +export default (event: any) => event.context.params!.param; diff --git a/test/vite/baseurl-dotted-param-fixture/index.html b/test/vite/baseurl-dotted-param-fixture/index.html new file mode 100644 index 0000000000..5a55be9846 --- /dev/null +++ b/test/vite/baseurl-dotted-param-fixture/index.html @@ -0,0 +1,11 @@ + + + + + + Nitro Base URL Dotted Param Fixture + + +
fixture
+ + diff --git a/test/vite/baseurl-dotted-param-fixture/tsconfig.json b/test/vite/baseurl-dotted-param-fixture/tsconfig.json new file mode 100644 index 0000000000..4b886bd47e --- /dev/null +++ b/test/vite/baseurl-dotted-param-fixture/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "nitro/tsconfig" +} diff --git a/test/vite/baseurl-dotted-param-fixture/vite.config.ts b/test/vite/baseurl-dotted-param-fixture/vite.config.ts new file mode 100644 index 0000000000..23f6a51841 --- /dev/null +++ b/test/vite/baseurl-dotted-param-fixture/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ + base: "/subdir/", + plugins: [ + nitro({ + baseURL: "/subdir/", + serverDir: "./", + serveStatic: false, + }), + ], +}); diff --git a/test/vite/baseurl-dotted-param.test.ts b/test/vite/baseurl-dotted-param.test.ts new file mode 100644 index 0000000000..c412e83584 --- /dev/null +++ b/test/vite/baseurl-dotted-param.test.ts @@ -0,0 +1,43 @@ +import { fileURLToPath } from "node:url"; +import type { ViteDevServer } from "vite"; +import { beforeAll, afterAll, describe, expect, test } from "vitest"; + +const { createServer } = (await import( + process.env.NITRO_VITE_PKG || "vite" +)) as typeof import("vite"); + +describe("vite:baseURL dotted params", { sequential: true }, () => { + let server: ViteDevServer; + let serverURL: string; + + const rootDir = fileURLToPath(new URL("./baseurl-dotted-param-fixture", import.meta.url)); + + beforeAll(async () => { + process.chdir(rootDir); + server = await createServer({ root: rootDir }); + await server.listen("0" as unknown as number); + const addr = server.httpServer?.address() as { + port: number; + address: string; + family: string; + }; + serverURL = `http://${addr.family === "IPv6" ? `[${addr.address}]` : addr.address}:${addr.port}`; + }, 30_000); + + afterAll(async () => { + await server?.close(); + }); + + test("serves Nitro API routes with dotted params under baseURL without redirecting", async () => { + const response = await fetch(`${serverURL}/subdir/api/proxy/todos/Package.todos.Entity.3`, { + headers: { + "sec-fetch-dest": "empty", + }, + redirect: "manual", + }); + + expect(response.status).toBe(200); + expect(response.headers.get("location")).toBeNull(); + expect(await response.text()).toBe("todos/Package.todos.Entity.3"); + }); +}); From f761fcf283f3795fcb483dcfcfd3caff8ab2f503 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 14 Mar 2026 23:49:50 +0100 Subject: [PATCH 2/3] update --- src/build/vite/dev.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/build/vite/dev.ts b/src/build/vite/dev.ts index 894cf10c11..747e274a9c 100644 --- a/src/build/vite/dev.ts +++ b/src/build/vite/dev.ts @@ -199,8 +199,12 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi return next(); } nodeReq._nitroHandled = true; + + const baseURL = nitro.options.baseURL || "/"; const originalURL = nodeReq.url; - nodeReq.url = withBase(nodeReq.url, nitro.options.baseURL); + if (baseURL !== "/") { + nodeReq.url = withBase(nodeReq.url, baseURL); + } try { // Create web API compat request const req = new NodeRequest({ req: nodeReq, res: nodeRes }); @@ -223,7 +227,9 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi } catch (error) { return next(error); } finally { - nodeReq.url = originalURL; + if (baseURL !== "/") { + nodeReq.url = originalURL; + } } }; From 8854f47b1536c68a02b735e819135e6ddd16a0ac Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 23 Mar 2026 23:58:50 +0100 Subject: [PATCH 3/3] test(vite): cover all sec-fetch-dest variants for dotted param test test document/empty/undefined fetch destinations to ensure the nitroDevMiddlewarePre fix is exercised (not just the catch-all) --- test/vite/baseurl-dotted-param.test.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/test/vite/baseurl-dotted-param.test.ts b/test/vite/baseurl-dotted-param.test.ts index c412e83584..e1cc39cd8a 100644 --- a/test/vite/baseurl-dotted-param.test.ts +++ b/test/vite/baseurl-dotted-param.test.ts @@ -29,15 +29,21 @@ describe("vite:baseURL dotted params", { sequential: true }, () => { }); test("serves Nitro API routes with dotted params under baseURL without redirecting", async () => { - const response = await fetch(`${serverURL}/subdir/api/proxy/todos/Package.todos.Entity.3`, { - headers: { - "sec-fetch-dest": "empty", - }, - redirect: "manual", - }); + for (const fetchDest of ["empty", "document", undefined]) { + const headers: Record = {}; + if (fetchDest) { + headers["sec-fetch-dest"] = fetchDest; + } + const response = await fetch(`${serverURL}/subdir/api/proxy/todos/Package.todos.Entity.3`, { + headers, + redirect: "manual", + }); - expect(response.status).toBe(200); - expect(response.headers.get("location")).toBeNull(); - expect(await response.text()).toBe("todos/Package.todos.Entity.3"); + expect(response.status, `sec-fetch-dest: ${fetchDest}`).toBe(200); + expect(response.headers.get("location"), `sec-fetch-dest: ${fetchDest}`).toBeNull(); + expect(await response.text(), `sec-fetch-dest: ${fetchDest}`).toBe( + "todos/Package.todos.Entity.3" + ); + } }); });