Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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`)
Expand Down
7 changes: 6 additions & 1 deletion packages/health/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,10 @@ export function resolveFormatError(
}

export function toError<T>(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");
}
Comment thread
thibaultleouay marked this conversation as resolved.
Outdated
}
45 changes: 45 additions & 0 deletions packages/health/src/responder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Ctx>({
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<Ctx>({
Expand All @@ -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<Ctx>({
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<Ctx>({
Expand Down
13 changes: 13 additions & 0 deletions packages/health/src/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading