From 5facd3afb1a1c642f8191b02c6399ae0b6ef7395 Mon Sep 17 00:00:00 2001 From: max-foss <> Date: Fri, 3 Apr 2026 15:40:07 +0200 Subject: [PATCH] fix: Return proper error code on config-related startup errors. --- lib/util/onboarding.ts | 10 ++++++-- test/onboarding.test.ts | 52 +++++++++++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/lib/util/onboarding.ts b/lib/util/onboarding.ts index e8e2c4082b..f6718c862d 100644 --- a/lib/util/onboarding.ts +++ b/lib/util/onboarding.ts @@ -224,17 +224,18 @@ async function startFailureServer(errors: string[]): Promise { const pathname = new URL(req.url /* v8 ignore next */ ?? "/", serverUrl).pathname; if (req.method === "GET" && pathname === "/data") { + // Also serve error code when underlying data fetched shows an error. const payload: OnboardFailureData = {page: "failure", errors}; res.setHeader("Content-Type", "application/json"); - res.writeHead(200); + res.writeHead(500); res.end(stringify(payload)); return; } if (req.method === "POST" && pathname === "/submit") { - res.writeHead(200); + res.writeHead(500); res.end(() => { resolve(); }); @@ -242,6 +243,11 @@ async function startFailureServer(errors: string[]): Promise { return; } + if (req.method === "GET" && (pathname === "/" || pathname === "/index.html")) { + // Also serve error code when underlying data fetched shows an error. + res.statusCode = 500; + } + const next = finalhandler(req, res); fileServer(req, res, next); diff --git a/test/onboarding.test.ts b/test/onboarding.test.ts index 08ba99855d..8739fddf25 100644 --- a/test/onboarding.test.ts +++ b/test/onboarding.test.ts @@ -343,7 +343,7 @@ describe("Onboarding", () => { await vi.advanceTimersByTimeAsync(100); // flush expect(resSetHeader).toHaveBeenNthCalledWith(1, "Content-Type", "application/json"); - expect(resWriteHead).toHaveBeenNthCalledWith(1, 200); + expect(resWriteHead).toHaveBeenNthCalledWith(1, 500); expect(resEnd).toHaveBeenCalledTimes(1); mockHttpListener( @@ -361,6 +361,7 @@ describe("Onboarding", () => { ); await responsePromise; + expect(resWriteHead).toHaveBeenNthCalledWith(2, 500); expect(resEnd).toHaveBeenCalledTimes(2); const serverUrl = new URL(process.env.Z2M_ONBOARD_URL ?? "http://0.0.0.0:8080"); @@ -506,7 +507,7 @@ describe("Onboarding", () => { return JSON.parse(resEnd.mock.calls[0][0]) as OnboardSubmitResponse; }; - const requestUnhandledRoute = async (url: string): Promise => { + const requestUnhandledRoute = async (url: string): Promise<{statusCode: number}> => { let resolveResponse: () => void = () => {}; const responsePromise = new Promise((resolve) => { resolveResponse = resolve; @@ -520,6 +521,12 @@ describe("Onboarding", () => { resolveResponse(); }); + const res = { + end: resEnd, + setHeader: vi.fn(), + writeHead: vi.fn(), + statusCode: 200, + }; mockHttpListener( { @@ -528,14 +535,11 @@ describe("Onboarding", () => { // @ts-expect-error return not used on: () => {}, }, - { - end: resEnd, - setHeader: vi.fn(), - writeHead: vi.fn(), - }, + res, ); await responsePromise; + return res; }; const createZipRestore = (): Awaited> => { @@ -838,6 +842,40 @@ describe("Onboarding", () => { expect(mockStaticFileServer).toHaveBeenCalled(); }); + it("serves failure page routes with HTTP 500", async () => { + settings.testing.clear(); + + const configFile = join(data.mockDir, "configuration.yaml"); + + writeFileSync( + configFile, + ` + good: 9 + \t wrong + `, + ); + + let p; + await new Promise((resolve, reject) => { + mockHttpOnListen.mockImplementationOnce(async () => { + try { + expect((await requestUnhandledRoute("/")).statusCode).toBe(500); + expect((await requestUnhandledRoute("/index.html")).statusCode).toBe(500); + await runFailure(); + resolve(); + } catch (error) { + reject(error); + } + }); + + p = onboard(); + }); + + await expect(p).resolves.toStrictEqual(false); + + data.removeConfiguration(); + }); + it("returns false when onboarding server emits an error", async () => { data.removeConfiguration();