From 32c534175780b333dd44e5490f2989f4aa5d785a Mon Sep 17 00:00:00 2001 From: clawbox Date: Tue, 4 Aug 2026 21:04:35 +1000 Subject: [PATCH 1/3] fix(gateway): bound permanent startup failures --- config/clawbox-gateway.service | 9 +- src/app/setup-api/gateway/route.ts | 27 ++++- src/lib/gateway-health.ts | 108 ++++++++++++++++++ src/tests/routes/gateway/gateway.test.ts | 46 ++++++++ src/tests/unit/gateway-health.test.ts | 55 +++++++++ .../unit/install-post-update-units.test.ts | 41 +++++++ 6 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 src/lib/gateway-health.ts create mode 100644 src/tests/unit/gateway-health.test.ts diff --git a/config/clawbox-gateway.service b/config/clawbox-gateway.service index 9b97e1920..fcccd0f1b 100644 --- a/config/clawbox-gateway.service +++ b/config/clawbox-gateway.service @@ -2,6 +2,12 @@ Description=ClawBox OpenClaw Gateway After=network-online.target Wants=network-online.target +# A cold Jetson can legitimately spend up to TimeoutStartSec in +# gateway-pre-start.sh. Keep the limiter window long enough to contain several +# complete slow failures; inherited 5-in-10s defaults never tripped when one +# failed cycle already took longer than ten seconds (issue #284). +StartLimitIntervalSec=3600 +StartLimitBurst=5 [Service] Type=simple @@ -13,7 +19,8 @@ ExecStartPre=/home/clawbox/clawbox/scripts/gateway-pre-start.sh # gateway-proxy.ts injects into the SPA). Passing a literal here would override # that and reintroduce the shared-token / UI-drift bug (issues #149, #150). ExecStart=/home/clawbox/.npm-global/bin/openclaw gateway --allow-unconfigured --bind lan -Restart=always +# Retry crashes and rejected startups, but leave deliberate clean stops alone. +Restart=on-failure RestartSec=5 # gateway-pre-start.sh runs as a blocking ExecStartPre. The default # ~90s start timeout could kill a legitimately slow first-boot pre-start diff --git a/src/app/setup-api/gateway/route.ts b/src/app/setup-api/gateway/route.ts index d814b5ee2..b6d513b47 100644 --- a/src/app/setup-api/gateway/route.ts +++ b/src/app/setup-api/gateway/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getGatewayToken } from "@/lib/gateway-proxy"; +import { getGatewayServiceHealth, type GatewayServiceHealth } from "@/lib/gateway-health"; export const dynamic = "force-dynamic"; @@ -15,7 +16,7 @@ export async function GET(request: NextRequest) { getGatewayToken(), ]); if (!res.ok) { - return gatewayOfflineResponse(); + return gatewayOfflineResponse(await getGatewayServiceHealth()); } let html = await res.text(); // Use the request hostname so WebSocket connects to the right address @@ -61,11 +62,23 @@ export async function GET(request: NextRequest) { }, }); } catch { - return gatewayOfflineResponse(); + return gatewayOfflineResponse(await getGatewayServiceHealth()); } } -function gatewayOfflineResponse() { +function escapeHtml(value: string): string { + return value.replace(/[&<>"']/g, (character) => ({ + "&": "&", "<": "<", ">": ">", '"': """, "'": "'", + })[character] ?? character); +} + +function gatewayOfflineResponse(health: GatewayServiceHealth) { + const breaker = health.breakerActive + ? `` + : ""; + const finalError = health.finalStartupError + ? `
${escapeHtml(health.finalStartupError)}
` + : ""; const html = `