Merge pull request #61 from arjunkomath/staging

Update OpenClaw & improve gateway health reporting
This commit is contained in:
Arjun Komath
2026-04-25 13:01:05 +10:00
committed by GitHub
5 changed files with 172 additions and 28 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ RUN apt-get update \
zip \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g openclaw@2026.4.21 clawhub@latest
RUN npm install -g openclaw@2026.4.23 clawhub@latest
WORKDIR /app
+2 -1
View File
@@ -8,7 +8,8 @@
"scripts": {
"dev": "node src/server.js",
"start": "node src/server.js",
"lint": "node -c src/server.js"
"lint": "node -c src/server.js",
"test": "node --test src/*.test.mjs"
},
"dependencies": {
"express": "^5.1.0",
+27
View File
@@ -0,0 +1,27 @@
export function canServeGatewayRequest({ configured, reachable }) {
if (!configured) return false;
return reachable;
}
export function describeGatewayHealth({
configured,
hasProcessHandle,
starting,
reachable,
}) {
const serving = canServeGatewayRequest({
configured,
reachable,
});
return {
gateway: !configured
? "unconfigured"
: serving
? "ready"
: "starting",
gatewayRunning: hasProcessHandle && !starting,
gatewayStarting: starting,
gatewayReachable: reachable,
statusCode: configured && !reachable && !starting ? 503 : 200,
};
}
+99
View File
@@ -0,0 +1,99 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
canServeGatewayRequest,
describeGatewayHealth,
} from "./gateway-readiness.js";
test("serves gateway requests when a detached gateway is reachable", () => {
assert.equal(
canServeGatewayRequest({
configured: true,
reachable: true,
}),
true,
);
});
test("reports a reachable detached gateway as ready", () => {
assert.deepEqual(
describeGatewayHealth({
configured: true,
hasProcessHandle: false,
starting: false,
reachable: true,
}),
{
gateway: "ready",
gatewayRunning: false,
gatewayStarting: false,
gatewayReachable: true,
statusCode: 200,
},
);
});
test("does not serve gateway requests while configured gateway is unreachable", () => {
assert.equal(
canServeGatewayRequest({
configured: true,
reachable: false,
}),
false,
);
});
test("does not treat a process handle as reachable gateway readiness", () => {
assert.deepEqual(
describeGatewayHealth({
configured: true,
hasProcessHandle: true,
starting: false,
reachable: false,
}),
{
gateway: "starting",
gatewayRunning: true,
gatewayStarting: false,
gatewayReachable: false,
statusCode: 503,
},
);
});
test("marks configured stopped gateway unhealthy for Railway recovery", () => {
assert.deepEqual(
describeGatewayHealth({
configured: true,
hasProcessHandle: false,
starting: false,
reachable: false,
}),
{
gateway: "starting",
gatewayRunning: false,
gatewayStarting: false,
gatewayReachable: false,
statusCode: 503,
},
);
});
test("keeps starting gateway healthy during boot grace", () => {
assert.deepEqual(
describeGatewayHealth({
configured: true,
hasProcessHandle: false,
starting: true,
reachable: false,
}),
{
gateway: "starting",
gatewayRunning: false,
gatewayStarting: true,
gatewayReachable: false,
statusCode: 200,
},
);
});
+43 -26
View File
@@ -8,6 +8,10 @@ import express from "express";
import httpProxy from "http-proxy";
import pty from "node-pty";
import { WebSocketServer } from "ws";
import {
canServeGatewayRequest,
describeGatewayHealth,
} from "./gateway-readiness.js";
const PORT = Number.parseInt(process.env.PORT ?? "8080", 10);
const STATE_DIR =
@@ -185,24 +189,34 @@ function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function probeGatewayOnce() {
async function probeGatewayOnce(opts = {}) {
const endpoints = ["/openclaw", "/", "/health"];
const timeoutMs = opts.timeoutMs ?? 2000;
for (const endpoint of endpoints) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(`${GATEWAY_TARGET}${endpoint}`, {
method: "GET",
signal: controller.signal,
});
if (res) {
if (res.status < 500) {
return { ok: true, endpoint };
}
} catch (err) {
if (err.code !== "ECONNREFUSED" && err.cause?.code !== "ECONNREFUSED") {
if (
err.name !== "AbortError" &&
err.code !== "ECONNREFUSED" &&
err.cause?.code !== "ECONNREFUSED"
) {
const msg = err.code || err.message;
if (msg !== "fetch failed" && msg !== "UND_ERR_CONNECT_TIMEOUT") {
log.warn("gateway", `health check error: ${msg}`);
}
}
} finally {
clearTimeout(timeout);
}
}
@@ -423,36 +437,32 @@ app.get("/styles.css", (_req, res) => {
});
app.get("/healthz", async (_req, res) => {
let gateway = "unconfigured";
if (isConfigured()) {
gateway = isGatewayReady() ? "ready" : "starting";
}
res.json({ ok: true, gateway });
const configured = isConfigured();
const health = describeGatewayHealth({
configured,
hasProcessHandle: isGatewayReady(),
starting: isGatewayStarting(),
reachable: configured ? (await probeGatewayOnce()).ok : false,
});
res.json({ ok: true, gateway: health.gateway });
});
app.get("/setup/healthz", async (_req, res) => {
const configured = isConfigured();
const gatewayRunning = isGatewayReady();
const starting = isGatewayStarting();
let gatewayReachable = false;
const health = describeGatewayHealth({
configured,
hasProcessHandle: isGatewayReady(),
starting: isGatewayStarting(),
reachable: configured ? (await probeGatewayOnce()).ok : false,
});
if (gatewayRunning) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const r = await fetch(`${GATEWAY_TARGET}/`, { signal: controller.signal });
clearTimeout(timeout);
gatewayReachable = r !== null;
} catch {}
}
res.json({
res.status(health.statusCode).json({
ok: true,
wrapper: true,
configured,
gatewayRunning,
gatewayStarting: starting,
gatewayReachable,
gatewayRunning: health.gatewayRunning,
gatewayStarting: health.gatewayStarting,
gatewayReachable: health.gatewayReachable,
});
});
@@ -1421,15 +1431,22 @@ app.use(async (req, res) => {
if (isConfigured()) {
if (!isGatewayReady()) {
let gatewayReachable = false;
try {
await ensureGatewayRunning();
gatewayReachable = (await probeGatewayOnce()).ok;
} catch {
return res
.status(503)
.sendFile(path.join(process.cwd(), "src", "public", "loading.html"));
}
if (!isGatewayReady()) {
if (
!canServeGatewayRequest({
configured: true,
reachable: gatewayReachable,
})
) {
return res
.status(503)
.sendFile(path.join(process.cwd(), "src", "public", "loading.html"));