diff --git a/CHANGES.md b/CHANGES.md index 71d32b8..a9216a3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## Unreleased +- Health reports and responder fallbacks remain available when a rejected value + cannot convert to a string. The error uses generic text without private fields. - Health responses ignore a top-level `toJSON` from `extend` so JSON serialization cannot replace the report fields. Nested dates and custom JSON values keep their normal serialization. diff --git a/packages/health/src/errors.ts b/packages/health/src/errors.ts index 8ed84f2..aac82b1 100644 --- a/packages/health/src/errors.ts +++ b/packages/health/src/errors.ts @@ -50,5 +50,10 @@ export function resolveFormatError( } export function toError(value: T): Error { - return value instanceof Error ? value : new Error(String(value)); + try { + if (value instanceof Error) return value; + return new Error(String(value)); + } catch { + return new Error("failed"); + } } diff --git a/packages/health/src/responder.test.ts b/packages/health/src/responder.test.ts index 601534a..1f0b457 100644 --- a/packages/health/src/responder.test.ts +++ b/packages/health/src/responder.test.ts @@ -119,6 +119,28 @@ test("createHealthResponder() reports a rejected extend and a thrown onError", a assert.equal(rendered.body.status, "ok"); }); +test("createHealthResponder() serves the report when an extend rejection cannot become a string", async () => { + const errors: Error[] = []; + const responder = createHealthResponder({ + probes: [failingProbe("a", true)], + extend: () => Promise.reject({ toString: 0, code: "PRIVATE_FAILURE" }), + onError: (error) => errors.push(error), + }); + const rendered = await responder.respond({}); + assert.equal(rendered.status, 503); + assert.equal(rendered.body.status, "unhealthy"); + assert.equal(rendered.body.checks?.[0].status, "failed"); + assert.deepEqual(Object.keys(rendered.body).sort(), [ + "checkedAt", + "checks", + "latencyMs", + "status", + ]); + assert.equal(errors.length, 1); + assert.ok(errors[0] instanceof Error); + assert.doesNotMatch(errors[0].message, /PRIVATE_FAILURE/); +}); + test("createHealthResponder() treats a throwing exposeChecks as false", async () => { const seen: Error[] = []; const responder = createHealthResponder({ @@ -137,6 +159,29 @@ test("createHealthResponder() treats a throwing exposeChecks as false", async () assert.equal(seen[0]?.message, "auth down"); }); +test("createHealthResponder() hides details when an exposeChecks rejection cannot become a string", async () => { + const errors: Error[] = []; + let extended = false; + const responder = createHealthResponder({ + probes: [failingProbe("a", true)], + exposeChecks: () => + Promise.reject({ toString: 0, code: "PRIVATE_FAILURE" }), + extend: () => { + extended = true; + return { secret: "private" }; + }, + onError: (error) => errors.push(error), + }); + const rendered = await responder.respond({}); + assert.equal(rendered.status, 503); + assert.equal(rendered.body.status, "unhealthy"); + assert.deepEqual(Object.keys(rendered.body).sort(), ["checkedAt", "status"]); + assert.equal(extended, false); + assert.equal(errors.length, 1); + assert.ok(errors[0] instanceof Error); + assert.doesNotMatch(errors[0].message, /PRIVATE_FAILURE/); +}); + test("createHealthResponder() passes the context to onError", async () => { let seen: Ctx | undefined; const responder = createHealthResponder({ diff --git a/packages/health/src/run.test.ts b/packages/health/src/run.test.ts index 724cd5b..1527e01 100644 --- a/packages/health/src/run.test.ts +++ b/packages/health/src/run.test.ts @@ -114,6 +114,19 @@ test("runProbes() passes real errors to formatError", async () => { assert.equal(report.checks[1].error, "E: raw"); }); +test("runProbes() reports critical rejections that cannot become strings", async () => { + const report = await runProbes([{ + name: "a", + critical: true, + run: () => Promise.reject({ toString: 0, code: "PRIVATE_FAILURE" }), + }], { formatError: "message" }); + assert.equal(report.status, "unhealthy"); + assert.equal(report.checks[0].status, "failed"); + assert.equal(report.checks[0].critical, true); + assert.equal(typeof report.checks[0].error, "string"); + assert.doesNotMatch(JSON.stringify(report), /PRIVATE_FAILURE/); +}); + test("runProbes() times out and aborts the signal", async () => { const report = await runProbes([hanging("a", true)]); assert.equal(report.status, "unhealthy");