From d9225e5a121922d26fdd4fdcff7d470f7e235eda Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 23 May 2026 17:31:14 +0900 Subject: [PATCH 1/4] test --- packages/plugin-vite/tests/dev_server_test.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/packages/plugin-vite/tests/dev_server_test.ts b/packages/plugin-vite/tests/dev_server_test.ts index ffc30f92ee2..ec72a95ae6f 100644 --- a/packages/plugin-vite/tests/dev_server_test.ts +++ b/packages/plugin-vite/tests/dev_server_test.ts @@ -1,5 +1,6 @@ import * as path from "@std/path"; import { expect } from "@std/expect"; +import { withTmpDir, writeFiles } from "../../fresh/src/test_utils.ts"; import { waitFor, waitForText, @@ -465,6 +466,87 @@ integrationTest( }, ); +// https://github.com/denoland/fresh/issues/3814 +integrationTest("vite dev - server.proxy bypasses Fresh routes", async () => { + const api = new URLPattern({ pathname: "/api/*" }); + const api2 = new URLPattern({ pathname: "/api2/*" }); + const api3 = new URLPattern({ pathname: "/api3/*" }); + await using proxy = Deno.serve({ + hostname: "127.0.0.1", + port: 0, + }, (req) => { + const url = new URL(req.url); + if (api.test({ pathname: url.pathname })) { + return new Response("api"); + } + if (api2.test({ pathname: url.pathname })) { + return new Response("api2"); + } + if (api3.test({ pathname: url.pathname })) { + return new Response("api3"); + } + return new Response(`${url.pathname}${url.search}`, { + status: 500, + }); + }); + + await using tmp = await withTmpDir({ + dir: path.join(import.meta.dirname!, ".."), + prefix: "tmp_vite_", + }); + + await writeFiles(tmp.dir, { + "main.ts": `import { App } from "@fresh/core"; +export const app = new App() +.get("/", () => new Response("ok")); +`, + "vite.config.ts": `import { defineConfig } from "vite"; +import { fresh } from "@fresh/plugin-vite"; + +export default defineConfig({ +plugins: [fresh()], +server: { + proxy: { + "/api": Deno.env.get("FRESH_TEST_PROXY_TARGET")!, + "/api2": { + target: Deno.env.get("FRESH_TEST_PROXY_TARGET")!, + rewrite: (path) => path.replace(/^\\/api2\\/ping/, "/api2/pong"), + }, + '^/api3/.*': { + target: Deno.env.get("FRESH_TEST_PROXY_TARGET")!, + changeOrigin: true, + }, + }, +}, +}); +`, + }); + + await launchDevServer( + tmp.dir, + async (address) => { + { + const res = await fetch(`${address}/api/ping?x=1`); + expect(res.status).toEqual(200); + expect(await res.text()).toEqual("api"); + } + { + const res = await fetch(`${address}/api2/pong?y=2`); + expect(res.status).toEqual(200); + expect(await res.text()).toEqual("api2"); + } + { + const res = await fetch(`${address}/api3/pong?z=3`); + expect(res.status).toEqual(200); + expect(await res.text()).toEqual("api3"); + } + }, + { + FRESH_TEST_PROXY_TARGET: `http://127.0.0.1:${proxy.addr.port}`, + }, + ); +}); + integrationTest("vite dev - source mapped stack traces", async () => { const res = await fetch(`${demoServer.address()}/tests/throw`); const text = await res.text(); From 7d5bbfdd3f6c33065b4c5c542425ffcdc1680ba6 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 23 May 2026 17:31:20 +0900 Subject: [PATCH 2/4] work --- .../plugin-vite/src/plugins/dev_server.ts | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/plugin-vite/src/plugins/dev_server.ts b/packages/plugin-vite/src/plugins/dev_server.ts index c1ca76d6c1f..03b6852add6 100644 --- a/packages/plugin-vite/src/plugins/dev_server.ts +++ b/packages/plugin-vite/src/plugins/dev_server.ts @@ -1,4 +1,4 @@ -import type { DevEnvironment, Plugin } from "vite"; +import type { DevEnvironment, Plugin, ResolvedServerOptions } from "vite"; import * as path from "@std/path"; import { contentType as getStdContentType } from "@std/media-types/content-type"; import { ASSET_CACHE_BUST_KEY } from "fresh/internal"; @@ -10,6 +10,34 @@ function getContentType(ext: string): string { return getStdContentType(ext) ?? "application/octet-stream"; } +/** + * Handling the user config of proxy + * https://vite.dev/config/server-options#server-proxy + */ +function createProxyUrlMatcher( + proxy: ResolvedServerOptions["proxy"], +): ((url: string) => boolean) | undefined { + if (proxy === undefined) return undefined; + + const matchers = Object.keys(proxy).map((context) => { + // with RegExp + if (context[0] === "^") { + const regex = new RegExp(context); + return (url: string) => regex.test(url); + } + // string shorthand + return (url: string) => url.startsWith(context); + }); + + return (url: string) => { + for (const matches of matchers) { + if (matches(url)) return true; + } + + return false; + }; +} + export function devServer(freshConfig: ResolvedFreshViteConfig): Plugin[] { let publicDir = ""; return [ @@ -27,15 +55,25 @@ export function devServer(freshConfig: ResolvedFreshViteConfig): Plugin[] { const IGNORE_URLS = new RegExp( `^(${base})?/(@(vite|fs|id)|\\.vite)/`, ); + // build proxy url list matcher beofre the request is coming + const matchesProxyUrl = createProxyUrlMatcher( + server.config.server.proxy, + ); server.middlewares.use(async (nodeReq, nodeRes, next) => { const serverCfg = server.config.server; + const rawUrl = nodeReq.url ?? "/"; + + // bypass the request when the proxy specified + if (matchesProxyUrl?.(rawUrl)) { + return next(); + } const protocol = serverCfg.https ? "https" : "http"; const host = serverCfg.host ? serverCfg.host : "localhost"; const port = serverCfg.port; const url = new URL( - `${protocol}://${host}:${port}${nodeReq.url ?? "/"}`, + `${protocol}://${host}:${port}${rawUrl}`, ); // Don't cache in dev From 3d3668ac53f760a2a7069b849146ce46f0a989a2 Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Sat, 23 May 2026 17:32:40 +0900 Subject: [PATCH 3/4] nit --- packages/plugin-vite/tests/dev_server_test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/plugin-vite/tests/dev_server_test.ts b/packages/plugin-vite/tests/dev_server_test.ts index ec72a95ae6f..44ea94e7086 100644 --- a/packages/plugin-vite/tests/dev_server_test.ts +++ b/packages/plugin-vite/tests/dev_server_test.ts @@ -485,9 +485,7 @@ integrationTest("vite dev - server.proxy bypasses Fresh routes", async () => { if (api3.test({ pathname: url.pathname })) { return new Response("api3"); } - return new Response(`${url.pathname}${url.search}`, { - status: 500, - }); + throw new Error("unreachable"); }); await using tmp = await withTmpDir({ From f4cb51eda0fb3a7adcad98d40baeb99353566fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Wed, 27 May 2026 07:49:38 +0200 Subject: [PATCH 4/4] address review feedback on proxy bypass - Fix typo and rephrase comment ("build proxy url list matcher beofre") - Add JSDoc note on the bypass() callback limitation - Add negative-case assertion to prove Fresh routes are still reachable --- packages/plugin-vite/src/plugins/dev_server.ts | 7 ++++++- packages/plugin-vite/tests/dev_server_test.ts | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/plugin-vite/src/plugins/dev_server.ts b/packages/plugin-vite/src/plugins/dev_server.ts index 03b6852add6..35fea312d52 100644 --- a/packages/plugin-vite/src/plugins/dev_server.ts +++ b/packages/plugin-vite/src/plugins/dev_server.ts @@ -13,6 +13,11 @@ function getContentType(ext: string): string { /** * Handling the user config of proxy * https://vite.dev/config/server-options#server-proxy + * + * Note: per-entry `bypass(req, res, options)` callbacks are not supported here. + * Requests matched by a proxy key are always short-circuited to Vite's proxy + * handler; a `bypass` that returns `false` would still skip Fresh but then + * fall through the proxy without being proxied, resulting in a 404. */ function createProxyUrlMatcher( proxy: ResolvedServerOptions["proxy"], @@ -55,7 +60,7 @@ export function devServer(freshConfig: ResolvedFreshViteConfig): Plugin[] { const IGNORE_URLS = new RegExp( `^(${base})?/(@(vite|fs|id)|\\.vite)/`, ); - // build proxy url list matcher beofre the request is coming + // Precompute the proxy URL matcher; proxy config is fixed at server start. const matchesProxyUrl = createProxyUrlMatcher( server.config.server.proxy, ); diff --git a/packages/plugin-vite/tests/dev_server_test.ts b/packages/plugin-vite/tests/dev_server_test.ts index 44ea94e7086..59ebdaf9f63 100644 --- a/packages/plugin-vite/tests/dev_server_test.ts +++ b/packages/plugin-vite/tests/dev_server_test.ts @@ -538,6 +538,12 @@ server: { expect(res.status).toEqual(200); expect(await res.text()).toEqual("api3"); } + { + // Ensure the bypass is selective — non-proxied routes still hit Fresh. + const res = await fetch(`${address}/`); + expect(res.status).toEqual(200); + expect(await res.text()).toEqual("ok"); + } }, { FRESH_TEST_PROXY_TARGET: `http://127.0.0.1:${proxy.addr.port}`,