From 541450fbd9b3589da39ced7dfc09cd4f91c5ac9a Mon Sep 17 00:00:00 2001 From: ephraimduncan Date: Mon, 14 Sep 2026 18:06:08 +0000 Subject: [PATCH 1/2] fix(health): handle errors that cannot convert to strings --- CHANGES.md | 5 +++ packages/health/src/errors.ts | 7 ++++- packages/health/src/responder.test.ts | 45 +++++++++++++++++++++++++++ packages/health/src/run.test.ts | 13 ++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 2099aa4..7828e02 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # Changelog +## 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. + ## 0.1.2 - Every hosting package (`fly`, `koyeb`, `railway`, `vercel`, `cloudflare`) diff --git a/packages/health/src/errors.ts b/packages/health/src/errors.ts index 8ed84f2..84077d2 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)); + if (value instanceof Error) return value; + try { + 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 e3d6f73..6bbbccb 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"); From 9487bf68f1387ac26f3f022670ea6ceb91482606 Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Tue, 15 Sep 2026 11:22:22 +0200 Subject: [PATCH 2/2] Update packages/health/src/errors.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- packages/health/src/errors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/src/errors.ts b/packages/health/src/errors.ts index 84077d2..aac82b1 100644 --- a/packages/health/src/errors.ts +++ b/packages/health/src/errors.ts @@ -50,8 +50,8 @@ export function resolveFormatError( } export function toError(value: T): Error { - if (value instanceof Error) return value; try { + if (value instanceof Error) return value; return new Error(String(value)); } catch { return new Error("failed");