fix(server): expose build version in health endpoint (#3404)

* fix(server): expose build version in health endpoint

* fix(server): redact public health failure details
This commit is contained in:
Amruth Pillai
2026-09-05 08:50:17 -07:00
committed by GitHub
parent 93e8d192a4
commit cd1c597ff0
5 changed files with 385 additions and 3 deletions
+96
View File
@@ -0,0 +1,96 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { execute, healthcheck } = vi.hoisted(() => ({ execute: vi.fn(), healthcheck: vi.fn() }));
vi.mock("@reactive-resume/db/client", () => ({ db: { execute } }));
vi.mock("@reactive-resume/api/features/storage", () => ({ getStorageService: () => ({ healthcheck }) }));
vi.mock("../app-version", () => ({ appVersion: "9.8.7" }));
import { handleHealth } from "./health";
describe("health version reporting", () => {
beforeEach(() => {
execute.mockResolvedValue([]);
healthcheck.mockResolvedValue({ status: "healthy" });
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it("reports the built application version when launched directly by Node", async () => {
vi.stubEnv("npm_package_version", undefined);
const response = await handleHealth();
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({ service: "reactive-resume", version: "9.8.7", status: "healthy" });
});
it("ignores a package manager's workspace package version", async () => {
vi.stubEnv("npm_package_version", "0.0.0");
expect(await (await handleHealth()).json()).toMatchObject({ version: "9.8.7" });
});
it("keeps the version available when a dependency is unhealthy", async () => {
vi.stubEnv("npm_package_version", undefined);
execute.mockRejectedValueOnce(new Error("Database unavailable"));
vi.spyOn(console, "warn").mockImplementation(() => {});
const response = await handleHealth();
expect(response.status).toBe(503);
expect(await response.json()).toMatchObject({ version: "9.8.7", status: "unhealthy" });
});
it.each(["database", "storage"])("keeps thrown %s error details in server logs only", async (dependency) => {
const detail = "Connection failed for private-user at internal.example:5432";
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
(dependency === "database" ? execute : healthcheck).mockRejectedValueOnce(new Error(detail));
const response = await handleHealth();
const body = await response.json();
expect(response.status).toBe(503);
expect(JSON.stringify(body)).not.toContain(detail);
expect(body[dependency]).toMatchObject({
status: "unhealthy",
error: expect.stringContaining("health check failed"),
});
expect(warn).toHaveBeenCalledWith(
"[Healthcheck]",
expect.objectContaining({
[dependency]: expect.objectContaining({ error: detail }),
}),
);
});
it("redacts returned storage failures while preserving diagnostics in server logs", async () => {
const detail = "Access denied to bucket private-bucket on internal.example";
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
healthcheck.mockResolvedValueOnce({
status: "unhealthy",
type: "s3",
message: detail,
error: detail,
internalDetail: detail,
});
const response = await handleHealth();
const body = await response.json();
expect(response.status).toBe(503);
expect(body.storage).toEqual({
status: "unhealthy",
type: "s3",
latencyMs: expect.any(Number),
error: "Storage health check failed.",
});
expect(JSON.stringify(body)).not.toContain(detail);
expect(warn).toHaveBeenCalledWith(
"[Healthcheck]",
expect.objectContaining({ storage: expect.objectContaining({ error: detail, message: detail }) }),
);
});
});
+14 -3
View File
@@ -2,6 +2,7 @@ import { sql } from "drizzle-orm";
import { withTimeout } from "es-toolkit";
import { getStorageService } from "@reactive-resume/api/features/storage";
import { db } from "@reactive-resume/db/client";
import { appVersion } from "../app-version";
const HEALTHCHECK_TIMEOUT_MS = 1_500;
@@ -31,6 +32,16 @@ async function runCheck(check: () => Promise<object>): Promise<CheckResult> {
}
}
function publicCheck(check: CheckResult, name: "Database" | "Storage"): CheckResult {
if (check.status === "healthy") return check;
return {
status: check.status,
latencyMs: check.latencyMs,
error: `${name} health check failed.`,
...(check.type === "local" || check.type === "s3" ? { type: check.type } : {}),
};
}
// ponytail: inner try/catches removed; runCheck's outer catch handles all errors
async function checkDatabase() {
await db.execute(sql`SELECT 1`);
@@ -45,12 +56,12 @@ export async function handleHealth() {
const checks = {
service: "reactive-resume",
version: process.env.npm_package_version,
version: appVersion,
status,
timestamp: new Date().toISOString(),
uptime: `${process.uptime().toFixed(2)}s`,
database,
storage,
database: publicCheck(database, "Database"),
storage: publicCheck(storage, "Storage"),
};
if (status === "unhealthy") {
+23
View File
@@ -80,6 +80,29 @@ describe("generateOpenApiSpec", () => {
});
}, 15_000);
it("documents the public health endpoint at its actual URL", async () => {
const spec = await generateSpec();
const health = spec.paths?.["/api/health"]?.get;
expect(health).toMatchObject({
operationId: "getHealth",
security: [],
servers: [{ url: "https://rxresu.me" }],
});
for (const status of ["200", "503"]) {
expect(health?.responses?.[status]).toMatchObject({
content: {
"application/json": {
schema: {
required: expect.arrayContaining(["service", "version", "status"]),
properties: { version: { type: "string" } },
},
},
},
});
}
});
it("uses the canonical input-side ResumeData schema in update requests", async () => {
const spec = (await generateSpec()) as GeneratedSpecView;
const { $schema: _dialect, ...canonicalInputSchema } = createResumeDataJsonSchema();
+48
View File
@@ -1,3 +1,4 @@
import type { OpenAPI } from "@orpc/openapi";
import { OpenAPIGenerator } from "@orpc/openapi";
import { JSON_SCHEMA_INPUT_REGISTRY, ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
import { downloadResumePdfProcedure } from "@reactive-resume/api/features/resume/export";
@@ -50,6 +51,31 @@ type GenerateOpenApiSpecOptions = {
version: string;
};
const healthDependencySchema = {
type: "object",
properties: {
status: { type: "string", enum: ["healthy", "unhealthy"] },
latencyMs: { type: "number" },
error: { type: "string", description: "Generic failure message. Detailed diagnostics are logged on the server." },
},
required: ["status", "latencyMs"],
additionalProperties: true,
} satisfies OpenAPI.SchemaObject;
const healthResponseSchema = {
type: "object",
properties: {
service: { type: "string", enum: ["reactive-resume"] },
version: { type: "string", description: "The running application's build version." },
status: { type: "string", enum: ["healthy", "unhealthy"] },
timestamp: { type: "string", format: "date-time" },
uptime: { type: "string" },
database: healthDependencySchema,
storage: healthDependencySchema,
},
required: ["service", "version", "status", "timestamp", "uptime", "database", "storage"],
} satisfies OpenAPI.SchemaObject;
export async function generateOpenApiSpec({ appUrl, version }: GenerateOpenApiSpecOptions) {
return await openAPIGenerator.generate(openAPIRouter, {
info: {
@@ -60,6 +86,28 @@ export async function generateOpenApiSpec({ appUrl, version }: GenerateOpenApiSp
contact: { name: "Amruth Pillai", email: "hello@amruthpillai.com", url: "https://amruthpillai.com" },
},
servers: [{ url: `${appUrl}/api/openapi` }],
paths: {
"/api/health": {
get: {
operationId: "getHealth",
tags: ["System"],
summary: "Get application health and version",
description: "Checks database and storage availability. Does not require authentication.",
servers: [{ url: appUrl }],
security: [],
responses: {
"200": {
description: "The application and its dependencies are healthy.",
content: { "application/json": { schema: healthResponseSchema } },
},
"503": {
description: "One or more application dependencies are unhealthy.",
content: { "application/json": { schema: healthResponseSchema } },
},
},
},
},
},
externalDocs: { url: "https://docs.rxresu.me", description: "Reactive Resume Documentation" },
commonSchemas: {
ResumeData: { schema: resumeDataSchema, strategy: "input" },
+204
View File
@@ -28878,6 +28878,210 @@
}
}
}
},
"/api/health": {
"get": {
"operationId": "getHealth",
"tags": [
"System"
],
"summary": "Get application health and version",
"description": "Checks database and storage availability. Does not require authentication.",
"servers": [
{
"url": "https://rxresu.me"
}
],
"security": [],
"responses": {
"200": {
"description": "The application and its dependencies are healthy.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"service": {
"type": "string",
"enum": [
"reactive-resume"
]
},
"version": {
"type": "string",
"description": "The running application's build version."
},
"status": {
"type": "string",
"enum": [
"healthy",
"unhealthy"
]
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"uptime": {
"type": "string"
},
"database": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"healthy",
"unhealthy"
]
},
"latencyMs": {
"type": "number"
},
"error": {
"type": "string",
"description": "Generic failure message. Detailed diagnostics are logged on the server."
}
},
"required": [
"status",
"latencyMs"
],
"additionalProperties": true
},
"storage": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"healthy",
"unhealthy"
]
},
"latencyMs": {
"type": "number"
},
"error": {
"type": "string",
"description": "Generic failure message. Detailed diagnostics are logged on the server."
}
},
"required": [
"status",
"latencyMs"
],
"additionalProperties": true
}
},
"required": [
"service",
"version",
"status",
"timestamp",
"uptime",
"database",
"storage"
]
}
}
}
},
"503": {
"description": "One or more application dependencies are unhealthy.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"service": {
"type": "string",
"enum": [
"reactive-resume"
]
},
"version": {
"type": "string",
"description": "The running application's build version."
},
"status": {
"type": "string",
"enum": [
"healthy",
"unhealthy"
]
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"uptime": {
"type": "string"
},
"database": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"healthy",
"unhealthy"
]
},
"latencyMs": {
"type": "number"
},
"error": {
"type": "string",
"description": "Generic failure message. Detailed diagnostics are logged on the server."
}
},
"required": [
"status",
"latencyMs"
],
"additionalProperties": true
},
"storage": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"healthy",
"unhealthy"
]
},
"latencyMs": {
"type": "number"
},
"error": {
"type": "string",
"description": "Generic failure message. Detailed diagnostics are logged on the server."
}
},
"required": [
"status",
"latencyMs"
],
"additionalProperties": true
}
},
"required": [
"service",
"version",
"status",
"timestamp",
"uptime",
"database",
"storage"
]
}
}
}
}
}
}
}
}
}