From a628fa887220a37676a8636b422d49cb9f07e331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Mon, 3 Aug 2026 10:51:35 +0200 Subject: [PATCH] fix(server): retry Windows reserved auto ports (#632) ## Summary - retry an ephemeral port when an automatic listener receives Windows `EACCES` - keep explicitly configured ports strict - keep non-Windows `EACCES` behavior unchanged ## Context CodeNomad already treats 9898 and 9899 as preferred rather than mandatory ports. The fallback previously handled only `EADDRINUSE`, so a Windows port reservation could terminate Tauri startup before the operating system was asked for an available port. The specific WinNAT attribution in the issue remains unverified, but the observable startup failure is fixed regardless of which Windows reservation produced `EACCES`. ## Validation - focused listener retry test passes - server typecheck passes - server suite: 256 passed, 4 skipped, 1 pre-existing Windows-only `git-worktrees.test.ts` fixture failure - autonomous gatekeeper review will be published as a PR comment after opening Fixes #627 --- packages/server/src/index.ts | 4 ++-- .../server/__tests__/listener-retry.test.ts | 18 ++++++++++++++++++ packages/server/src/server/http-server.ts | 14 +++++++------- 3 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 packages/server/src/server/__tests__/listener-retry.test.ts diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 0f06a2cf..3993ed7a 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -278,8 +278,8 @@ function resolveHost(input: string | undefined): string { return trimmed } -function programHasArg(argv: string[], flag: string): boolean { - return argv.includes(flag) +export function programHasArg(argv: string[], flag: string): boolean { + return argv.some((argument) => argument === flag || argument.startsWith(`${flag}=`)) } async function main() { diff --git a/packages/server/src/server/__tests__/listener-retry.test.ts b/packages/server/src/server/__tests__/listener-retry.test.ts new file mode 100644 index 00000000..6da41b81 --- /dev/null +++ b/packages/server/src/server/__tests__/listener-retry.test.ts @@ -0,0 +1,18 @@ +import assert from "node:assert/strict" +import test from "node:test" + +import { programHasArg } from "../../index" +import { shouldRetryPreferredPort } from "../http-server" + +test("automatic listeners retry Windows reserved ports without masking explicit failures", () => { + assert.equal(shouldRetryPreferredPort({ code: "EADDRINUSE" }, true, "linux"), true) + assert.equal(shouldRetryPreferredPort({ code: "EACCES" }, true, "win32"), true) + assert.equal(shouldRetryPreferredPort({ code: "EACCES" }, true, "linux"), false) + assert.equal(shouldRetryPreferredPort({ code: "EACCES" }, false, "win32"), false) +}) + +test("explicit listener ports are detected in both supported CLI forms", () => { + assert.equal(programHasArg(["--http-port", "9899"], "--http-port"), true) + assert.equal(programHasArg(["--https-port=9898"], "--https-port"), true) + assert.equal(programHasArg(["--http-porter=9899"], "--http-port"), false) +}) diff --git a/packages/server/src/server/http-server.ts b/packages/server/src/server/http-server.ts index 8df2832b..44e855b2 100644 --- a/packages/server/src/server/http-server.ts +++ b/packages/server/src/server/http-server.ts @@ -82,6 +82,12 @@ interface HttpServerStartResult { displayHost: string } +export function shouldRetryPreferredPort(error: unknown, autoPortRequested: boolean, platform = process.platform): boolean { + if (!autoPortRequested) return false + const code = (error as NodeJS.ErrnoException | undefined)?.code + return code === "EADDRINUSE" || (platform === "win32" && code === "EACCES") +} + export function createHttpServer(deps: HttpServerDeps) { // Fastify's type-level RawServer inference gets noisy when toggling HTTP vs HTTPS. // We keep the runtime behavior correct and cast the instance to a generic FastifyInstance. @@ -346,18 +352,12 @@ export function createHttpServer(deps: HttpServerDeps) { const autoPortRequested = deps.bindPort === 0 const primaryPort = autoPortRequested ? deps.defaultPort : deps.bindPort - const shouldRetryWithEphemeral = (error: unknown) => { - if (!autoPortRequested) return false - const err = error as NodeJS.ErrnoException | undefined - return Boolean(err && err.code === "EADDRINUSE") - } - let listenResult try { listenResult = await attemptListen(primaryPort) } catch (error) { - if (!shouldRetryWithEphemeral(error)) { + if (!shouldRetryPreferredPort(error, autoPortRequested)) { throw error } deps.logger.warn({ err: error, port: primaryPort }, "Preferred port unavailable, retrying on ephemeral port")