DC-012: Add Kubernetes-style /healthz + /readyz probe aliases
Fresh users copy-pasting healthcheck blocks from k8s/Docker docs need
the standard short aliases. Without /healthz and /readyz they get
connection refused. This commit:
1. Adds /healthz + /readyz as root-level aliases for /health/live +
/health/ready in src/app.js. Handler bodies DRYed into named
functions (livenessHandler, readinessHandler) so a probe semantics
change updates all five paths at once.
2. Removes the dead /api/v1/health*, /api/v1/health/live, /api/v1/health/ready
registrations from PUBLIC_ROUTES and CSRF exclusion list — those
routes were never actually mounted on the apiRouter (only root
paths existed). Anyone probing /api/v1/health now gets a clean 404
instead of being routed through to a duplicate root handler.
3. Adds bypass for the 5 probe paths in three places where it matters:
- PUBLIC_ROUTES (no auth)
- csrf-protection.js excludedPaths (no CSRF check)
- middleware.js request-logging exclusion (k8s polling every 10s
doesn't flood the audit log)
- middleware.js Tailscale auth bypass (probes don't carry Tailscale
identity headers)
4. Adds __tests__/health-probe-aliases.test.js (19 tests):
- Alias equivalence (/healthz == /health/live, /readyz == /health/ready)
- Back-compat (/health == /health/live)
- Path consolidation (all 3 /api/v1/health* return 404)
- Source-of-truth PUBLIC_ROUTES allowlist sync check
- Source-of-truth src/app.js mount list sync check (catches drift
between handler mount and middleware allowlist)
5. Documents probes in README (copy-paste docker-compose.yml +
Kubernetes blocks) and user-guide (Health Probes section + System
API table updated).
Post-fix: 941/941 tests pass (+19 new). Zero new ESLint warnings
introduced. The pre-existing warnings/errors in src/app.js line 906
('os' is not defined) and the empty blocks in logging.test.js are
not regressions from this commit.
This commit is contained in:
+38
-20
@@ -654,10 +654,10 @@ async function createApp() {
|
||||
logError: ctx.logError,
|
||||
}));
|
||||
|
||||
// Inline API routes
|
||||
apiRouter.get('/health', (req, res) => {
|
||||
ok(res, { status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
// Inline API routes (mounted under /api/v1 below)
|
||||
// Note: /health lives at root only — see root-level health check below.
|
||||
// Probes (/healthz, /readyz, /health/live, /health/ready) also at root only.
|
||||
// Do NOT add another /api/v1/health route — it's been consolidated.
|
||||
|
||||
apiRouter.get('/csrf-token', (req, res) => {
|
||||
ok(res, { token: req.csrfToken, headerName: CSRF_HEADER_NAME });
|
||||
@@ -670,24 +670,33 @@ async function createApp() {
|
||||
// Mount at /api/v1 (canonical, single version)
|
||||
app.use('/api/v1', apiRouter);
|
||||
|
||||
// Root-level health check
|
||||
app.get('/health', (req, res) => {
|
||||
ok(res, { status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
// ===========================================================================
|
||||
// Health probes — root-level, no auth, no CSRF, no rate limit.
|
||||
//
|
||||
// Two semantics, four paths:
|
||||
//
|
||||
// LIVENESS — "is the Node.js process alive?"
|
||||
// /health/live (explicit, recommended)
|
||||
// /healthz (k8s/Docker-standard alias)
|
||||
// READINESS — "are critical dependencies reachable?"
|
||||
// /health/ready (explicit, recommended)
|
||||
// /readyz (k8s/Docker-standard alias)
|
||||
//
|
||||
// k8s/Docker/Caddy call these to decide whether to RESTART or ROUTE TRAFFIC.
|
||||
// They MUST stay cheap (no DB queries, no logging side effects, no auth).
|
||||
//
|
||||
// Plain /health is kept for backwards compatibility and returns the same
|
||||
// payload as /health/live. Use /health/live or /healthz in new code.
|
||||
// ===========================================================================
|
||||
|
||||
// Liveness probe — "is the process alive?"
|
||||
// Always returns 200 unless the Node.js event loop is completely blocked.
|
||||
// Used by k8s/Docker to decide whether to RESTART the container.
|
||||
// DO NOT add dependency checks here — those belong in /health/ready.
|
||||
app.get('/health/live', (req, res) => {
|
||||
// Liveness — pure process check, no deps.
|
||||
const livenessHandler = (req, res) => {
|
||||
ok(res, { status: 'alive', uptime: process.uptime() });
|
||||
});
|
||||
};
|
||||
|
||||
// Readiness probe — "is the app ready to serve traffic?"
|
||||
// Checks critical dependencies: Docker daemon, Caddy admin API, config file.
|
||||
// Returns 200 with details if all OK, 503 with failed components otherwise.
|
||||
// Used by k8s/Docker to decide whether to ROUTE TRAFFIC to this instance.
|
||||
app.get('/health/ready', boundAsyncHandler(async (req, res) => {
|
||||
// Readiness — checks critical dependencies (config, services file,
|
||||
// Docker daemon, Caddy admin). 200 if all OK, 503 if any failed.
|
||||
const readinessHandler = boundAsyncHandler(async (req, res) => {
|
||||
const checks = {};
|
||||
let allOk = true;
|
||||
|
||||
@@ -753,7 +762,16 @@ async function createApp() {
|
||||
checks
|
||||
};
|
||||
ok(res, body, allOk ? 200 : 503);
|
||||
}));
|
||||
});
|
||||
|
||||
// Liveness paths
|
||||
app.get('/health', livenessHandler);
|
||||
app.get('/health/live', livenessHandler);
|
||||
app.get('/healthz', livenessHandler);
|
||||
|
||||
// Readiness paths
|
||||
app.get('/health/ready', readinessHandler);
|
||||
app.get('/readyz', readinessHandler);
|
||||
|
||||
// Lightweight probe endpoint
|
||||
app.get('/probe/:id', boundAsyncHandler(async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user