From a75a3007de416aac4be8417a242ffb3c4a726365 Mon Sep 17 00:00:00 2001 From: rowkav09 Date: Sat, 10 Oct 2026 00:48:43 +0100 Subject: [PATCH] fix: treat only a refused shutdown request as the app being absent The initial shutdown POST mapped every failure except a timeout to "not-running", so a reset or a broken reply from a live app let setup continue as if nothing were running. Only connection-refused proves absence, the same rule the wait loop already follows; anything else is "unavailable". Fixes #1123 --- src/windows-shutdown.js | 7 ++++--- test/windows-shutdown.test.js | 8 +++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/windows-shutdown.js b/src/windows-shutdown.js index f286adcb..66a06bcf 100644 --- a/src/windows-shutdown.js +++ b/src/windows-shutdown.js @@ -26,9 +26,10 @@ export async function requestLocalShutdown({ port, token, fetchImpl = fetch, req signal: AbortSignal.timeout(requestTimeoutMs), }); } catch (error) { - // A timeout means something else answered (or nothing parsed); a refused - // connection means the app is not running. - return error?.name === "TimeoutError" || error?.name === "AbortError" ? "unavailable" : "not-running"; + // Only a refused connection proves the app is not running. Timeouts, + // resets and other failures can come from a live app that answered + // badly; report unavailable so setup does not walk over it. + return error?.code === "ECONNREFUSED" || error?.cause?.code === "ECONNREFUSED" ? "not-running" : "unavailable"; } if (reply.status !== 202) return "unavailable"; // The app quits after answering; wait for the port to stop responding. diff --git a/test/windows-shutdown.test.js b/test/windows-shutdown.test.js index b703aea0..8f7b4ccb 100644 --- a/test/windows-shutdown.test.js +++ b/test/windows-shutdown.test.js @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { requestLocalShutdown, STOP_EXIT } from "../src/windows-shutdown.js"; test("a refused connection means the app is not running", async () => { - const outcome = await requestLocalShutdown({ port: 47832, token: "t", fetchImpl: async () => { throw new Error("fetch failed"); } }); + const outcome = await requestLocalShutdown({ port: 47832, token: "t", fetchImpl: async () => { throw Object.assign(new Error("connect ECONNREFUSED"), { code: "ECONNREFUSED" }); } }); assert.equal(outcome, "not-running"); }); @@ -82,3 +82,9 @@ test('#815 a hanging health probe is not proof the listener stopped', async () = assert.equal((await fetch(`http://127.0.0.1:${server.address().port}/`, { method: 'POST' })).status, 202); } finally { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); } }); + +test("a reset on the shutdown request is unavailable, not not-running (#1123)", async () => { + const reset = Object.assign(new Error("read ECONNRESET"), { code: "ECONNRESET" }); + const outcome = await requestLocalShutdown({ port: 47832, token: "t", fetchImpl: async () => { throw reset; } }); + assert.equal(outcome, "unavailable"); +});