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
This commit is contained in:
Pascal André 2026-08-03 10:51:35 +02:00 committed by GitHub
parent 598353db5e
commit a628fa8872
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 27 additions and 9 deletions

View file

@ -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() {

View file

@ -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)
})

View file

@ -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")