From 03f8806e1cc7093eada36850c88e3fae78fc4878 Mon Sep 17 00:00:00 2001 From: sanjibani <18418553+sanjibani@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:24:16 +0530 Subject: [PATCH] fix(plugin-vite): forward non-Fresh 404s to next middleware (e.g. server.proxy) Fresh's dev-server middleware previously short-circuited every request that reached it: when Fresh's router returned 404 (no matching route), the middleware wrote the 404 response and never called next(). That swallowed Vite's `server.proxy` middleware (and any user-installed middleware that runs after Fresh), so a config like: server: { proxy: { '/api': 'http://127.0.0.1:8000' } } returned 404 for /api/* requests instead of forwarding them. Fix: when Fresh's handler returns 404, call next() so the request can be handled by vite's proxy middleware or any downstream handler. Match the existing pattern for static files and Vite internal URLs. Adds a regression test with a separate vite.config.ts fixture (server_proxy) that proxies /api to a deno.serve target. Test verifies the target receives the forwarded request after my fix lands. Closes #3814. --- .../plugin-vite/src/plugins/dev_server.ts | 9 +++ packages/plugin-vite/tests/dev_server_test.ts | 79 +++++++++++++++++++ .../tests/fixtures/server_proxy/main.ts | 3 + .../fixtures/server_proxy/proxy_target.ts | 7 ++ .../fixtures/server_proxy/vite.config.ts | 15 ++++ 5 files changed, 113 insertions(+) create mode 100644 packages/plugin-vite/tests/fixtures/server_proxy/main.ts create mode 100644 packages/plugin-vite/tests/fixtures/server_proxy/proxy_target.ts create mode 100644 packages/plugin-vite/tests/fixtures/server_proxy/vite.config.ts diff --git a/packages/plugin-vite/src/plugins/dev_server.ts b/packages/plugin-vite/src/plugins/dev_server.ts index a69a4dbc57d..7e47a985eba 100644 --- a/packages/plugin-vite/src/plugins/dev_server.ts +++ b/packages/plugin-vite/src/plugins/dev_server.ts @@ -190,6 +190,15 @@ export function devServer(freshConfig: ResolvedFreshViteConfig): Plugin[] { const res = (await mod.default.fetch(req)) as Response; + // If Fresh didn't match a route, hand the request off to the + // next middleware (Vite's `server.proxy`, custom user + // middlewares, the SPA fallback, etc.). Without this, requests + // to `/api/*` proxy targets or other prefixed routes would be + // swallowed by a 404 from Fresh. See #3814. + if (res.status === 404) { + return next(); + } + // Collect css eagerly to avoid FOUC. This is a workaround for // Vite not supporting css natively. It's a bit hacky, but // gets the job done. diff --git a/packages/plugin-vite/tests/dev_server_test.ts b/packages/plugin-vite/tests/dev_server_test.ts index 1572154f96a..194c71ae0c2 100644 --- a/packages/plugin-vite/tests/dev_server_test.ts +++ b/packages/plugin-vite/tests/dev_server_test.ts @@ -119,6 +119,85 @@ integrationTest("vite dev - starts without routes/ dir", async () => { }); }); +// Regression test for https://github.com/freshframework/fresh/issues/3814 — +// vite's `server.proxy` config was being swallowed by Fresh's dev-server +// middleware. When Fresh's handler returned a 404 (because no route matched +// the proxy target path), the middleware terminated the request instead of +// calling next() to let the proxy middleware run. +integrationTest("vite dev - forwards server.proxy traffic", async () => { + const fixture = path.join(FIXTURE_DIR, "server_proxy"); + // Copy the fixture files manually so prepareDevServer doesn't overwrite + // vite.config.ts (it injects a minimal default config). + const tmpDir = await Deno.makeTempDir({ prefix: "tmp_proxy_" }); + try { + for ( + const entry of [ + "main.ts", + "proxy_target.ts", + "vite.config.ts", + ] + ) { + await Deno.copyFile( + path.join(fixture, entry), + path.join(tmpDir, entry), + ); + } + + // Boot the proxy target on an ephemeral port; pass the port to vite + // via env so its vite.config.ts can read it. + const proxyTarget = new Deno.Command(Deno.execPath(), { + cwd: tmpDir, + args: ["run", "-A", "proxy_target.ts"], + env: { NO_COLOR: "1" }, + stdout: "piped", + stderr: "piped", + }).spawn(); + + // Deno.serve prints "Listening on http://127.0.0.1:NNNN" to stderr. + let proxyPort = 0; + const reader = proxyTarget.stderr.pipeThrough(new TextDecoderStream()) + .getReader(); + for (let i = 0; i < 50; i++) { + const { value, done } = await reader.read(); + if (done) break; + const m = /Listening on http:\/\/[^\s]+:(\d+)/.exec(value ?? ""); + if (m) { + proxyPort = Number(m[1]); + break; + } + } + reader.cancel(); + + if (proxyPort === 0) { + proxyTarget.kill("SIGTERM"); + throw new Error("proxy_target.ts never printed its listening port"); + } + + try { + await launchDevServer(tmpDir, async (address) => { + // `/api/ping` is not a Fresh route — without the fix Fresh + // returns 404 and short-circuits the request. With the fix, + // vite's proxy middleware runs and forwards to proxy_target.ts. + const res = await fetch(`${address}/api/ping`); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.proxyReceived).toBe(true); + expect(body.pathname).toBe("/api/ping"); + }, { PROXY_TARGET_PORT: String(proxyPort) }); + } finally { + proxyTarget.kill("SIGTERM"); + } + } finally { + if (Deno.env.get("CI") !== "true") { + try { + await Deno.remove(tmpDir, { recursive: true }); + } catch { + // ignore + } + } + } +}); + integrationTest({ name: "vite dev - can apply HMR to islands (hooks)", ignore: true, // Test is very flaky diff --git a/packages/plugin-vite/tests/fixtures/server_proxy/main.ts b/packages/plugin-vite/tests/fixtures/server_proxy/main.ts new file mode 100644 index 00000000000..3456f10aa6d --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/server_proxy/main.ts @@ -0,0 +1,3 @@ +import { App } from "@fresh/core"; + +export const app = new App().get("/", () => new Response("ok")); diff --git a/packages/plugin-vite/tests/fixtures/server_proxy/proxy_target.ts b/packages/plugin-vite/tests/fixtures/server_proxy/proxy_target.ts new file mode 100644 index 00000000000..b1fdd56e822 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/server_proxy/proxy_target.ts @@ -0,0 +1,7 @@ +Deno.serve({ port: 0, hostname: "127.0.0.1" }, (req) => { + const url = new URL(req.url); + return Response.json({ + proxyReceived: true, + pathname: url.pathname, + }); +}); diff --git a/packages/plugin-vite/tests/fixtures/server_proxy/vite.config.ts b/packages/plugin-vite/tests/fixtures/server_proxy/vite.config.ts new file mode 100644 index 00000000000..4e8ff8d02a3 --- /dev/null +++ b/packages/plugin-vite/tests/fixtures/server_proxy/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import { fresh } from "@fresh/plugin-vite"; + +const proxyTargetPort = Number(process.env.PROXY_TARGET_PORT ?? 0); + +export default defineConfig({ + plugins: [fresh()], + server: { + host: "127.0.0.1", + port: 0, + proxy: { + "/api": `http://127.0.0.1:${proxyTargetPort}`, + }, + }, +});