From f119cacbc3176c0d1d0bd65b3adf3363fa5445c7 Mon Sep 17 00:00:00 2001 From: huyengiang101086-spec Date: Wed, 30 Sep 2026 08:12:31 +0800 Subject: [PATCH] fix(frontend): bind Windows dev server to loopback by default (#6034) Co-authored-by: huyengiang101086-spec <270684046+huyengiang101086-spec@users.noreply.github.com> --- frontend/AGENTS.md | 2 ++ frontend/scripts/dev.mjs | 20 ++++++++++++- frontend/tests/unit/scripts/dev.test.ts | 40 ++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index ca3f4869eb..1aec6887f3 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -40,6 +40,8 @@ Unit tests live under `tests/unit/` and mirror the `src/` layout (e.g., `tests/u Webpack is the default development bundler. Use `DEER_FLOW_DEV_BUNDLER=turbo` with `pnpm dev` to opt in to Turbopack when diagnosing a local Next.js bundler issue. +On Windows `pnpm dev` binds `127.0.0.1` by default because Hyper-V/winnat excluded port ranges can reject Next's default `0.0.0.0` bind with `EACCES` (#2870). Pass `pnpm dev -- --hostname 0.0.0.0` to listen on a LAN interface instead. + Rstest runs them as two projects (`rstest.config.ts`). `*.test.ts` / `*.test.tsx` run in a plain **node** environment — that is nearly the whole suite, and it is the default for anything that is pure logic. `*.dom.test.ts` / `*.dom.test.tsx` run in **happy-dom**, for tests that need a document: hooks driven through `renderHook` from `@testing-library/react`, and components. Keep the split — a DOM environment costs roughly 3x the runtime of the node suite, so tests that do not render should not opt into it. A hook whose behavior only exists under real React (effect ordering, cleanup on unmount, re-render on store change) belongs in a `.dom.test.*` file rather than a node test that mocks `react` itself. E2E tests live under `tests/e2e/` and use Playwright with Chromium. They mock all backend APIs via `page.route()` network interception and test real page interactions (navigation, chat input, streaming responses). Config: `playwright.config.ts`. The real-backend auth contract in `tests/e2e-real-backend/auth-disabled-contract.spec.ts` and `backend/tests/test_auth_me_permissions.py` pin the complete route-permission list; update both when adding registered permissions (including `projects:read/write/delete`). diff --git a/frontend/scripts/dev.mjs b/frontend/scripts/dev.mjs index 381a8535be..e3062fb6c5 100644 --- a/frontend/scripts/dev.mjs +++ b/frontend/scripts/dev.mjs @@ -25,6 +25,16 @@ export function getDevBundler(_platform = process.platform, env = process.env) { return "webpack"; } +/** + * @param {string[]} nextArgs + */ +function hasHostnameOverride(nextArgs) { + return nextArgs.some( + (arg) => + arg === "-H" || arg === "--hostname" || arg.startsWith("--hostname="), + ); +} + /** * @param {string} platform * @param {string[]} extraArgs @@ -36,7 +46,15 @@ export function getNextDevArgs( env = process.env, ) { const nextArgs = extraArgs[0] === "--" ? extraArgs.slice(1) : extraArgs; - return ["dev", `--${getDevBundler(platform, env)}`, ...nextArgs]; + const args = ["dev", `--${getDevBundler(platform, env)}`, ...nextArgs]; + // Windows reserves dynamic TCP port ranges (Hyper-V / WSL2 / winnat) that can + // make Next's default 0.0.0.0 bind fail with EACCES while a loopback bind on + // the same port succeeds. Bind loopback by default; an explicit + // --hostname/-H passthrough still opts into a wider interface. + if (platform === "win32" && !hasHostnameOverride(nextArgs)) { + args.push("--hostname", "127.0.0.1"); + } + return args; } function startDevServer() { diff --git a/frontend/tests/unit/scripts/dev.test.ts b/frontend/tests/unit/scripts/dev.test.ts index 1dcf3f26b1..6b87944ce6 100644 --- a/frontend/tests/unit/scripts/dev.test.ts +++ b/frontend/tests/unit/scripts/dev.test.ts @@ -24,6 +24,39 @@ describe("frontend dev launcher", () => { "--webpack", "--port", "3302", + "--hostname", + "127.0.0.1", + ]); + }); + + test("binds loopback by default on Windows only", () => { + expect(getNextDevArgs("win32")).toEqual([ + "dev", + "--webpack", + "--hostname", + "127.0.0.1", + ]); + expect(getNextDevArgs("linux")).toEqual(["dev", "--webpack"]); + expect(getNextDevArgs("darwin")).toEqual(["dev", "--webpack"]); + }); + + test("keeps an explicit Windows hostname passthrough", () => { + expect(getNextDevArgs("win32", ["--", "--hostname", "0.0.0.0"])).toEqual([ + "dev", + "--webpack", + "--hostname", + "0.0.0.0", + ]); + expect(getNextDevArgs("win32", ["--", "--hostname=0.0.0.0"])).toEqual([ + "dev", + "--webpack", + "--hostname=0.0.0.0", + ]); + expect(getNextDevArgs("win32", ["--", "-H", "0.0.0.0"])).toEqual([ + "dev", + "--webpack", + "-H", + "0.0.0.0", ]); }); @@ -31,7 +64,12 @@ describe("frontend dev launcher", () => { expect(getDevBundler("win32")).toBe("webpack"); expect(getDevBundler("linux")).toBe("webpack"); expect(getDevBundler("darwin")).toBe("webpack"); - expect(getNextDevArgs("win32")).toEqual(["dev", "--webpack"]); + expect(getNextDevArgs("win32")).toEqual([ + "dev", + "--webpack", + "--hostname", + "127.0.0.1", + ]); expect(getNextDevArgs("linux")).toEqual(["dev", "--webpack"]); }); });