fix(gateway): probe /api/health instead of root URL (#394)

* fix(gateway): probe /api/health instead of root URL for health checks (#390)

The server-side gateway health probe was fetching the root URL (/)
which returns HTTP 400 on OpenClaw 2026.3.13 gateways. The gateway
exposes a dedicated /api/health endpoint that returns 200 with status
info.

The WebSocket ping RPC 'unknown method' issue is already handled —
websocket.ts detects the INVALID_REQUEST and falls back to passive
heartbeat mode. The actual bug was this HTTP probe hitting the wrong
endpoint.

Fixes #390

* fix(gateway): ensure gateways table exists before health probe

The gateways table is created lazily by the gateways API (ensureTable).
The health route was querying it directly without CREATE IF NOT EXISTS,
causing SqliteError: no such table: gateways in fresh databases (E2E tests,
Docker first-boot).

Add ensureGatewaysTable() inline to mirror the pattern in route.ts.

* fix: update health-utils test to match /api/health probe path

The test file has its own copy of buildGatewayProbeUrl — update it to
append /api/health instead of / to match the route.ts change.

---------

Co-authored-by: Nyk <0xnykcd@googlemail.com>
This commit is contained in:
HonzysClawdbot 2026-03-16 05:55:56 +01:00 committed by GitHub
parent 0acf7daf32
commit 301ee9cdd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 5 additions and 5 deletions

View File

@ -80,7 +80,7 @@ function buildGatewayProbeUrl(host: string, port: number): string | null {
if (!parsed.port && Number.isFinite(port) && port > 0) { if (!parsed.port && Number.isFinite(port) && port > 0) {
parsed.port = String(port) parsed.port = String(port)
} }
if (!parsed.pathname) parsed.pathname = '/' parsed.pathname = parsed.pathname.replace(/\/+$/, '') + '/api/health'
return parsed.toString() return parsed.toString()
} catch { } catch {
return null return null
@ -88,7 +88,7 @@ function buildGatewayProbeUrl(host: string, port: number): string | null {
} }
if (!Number.isFinite(port) || port <= 0) return null if (!Number.isFinite(port) || port <= 0) return null
return `http://${rawHost}:${port}/` return `http://${rawHost}:${port}/api/health`
} }
function parseGatewayVersion(headers: Record<string, string | null>): string | null { function parseGatewayVersion(headers: Record<string, string | null>): string | null {
@ -180,7 +180,7 @@ describe('isBlockedUrl', () => {
describe('buildGatewayProbeUrl', () => { describe('buildGatewayProbeUrl', () => {
it('builds URL from bare host + port', () => { it('builds URL from bare host + port', () => {
expect(buildGatewayProbeUrl('example.com', 8080)).toBe('http://example.com:8080/') expect(buildGatewayProbeUrl('example.com', 8080)).toBe('http://example.com:8080/api/health')
}) })
it('preserves https:// protocol', () => { it('preserves https:// protocol', () => {

View File

@ -144,7 +144,7 @@ function buildGatewayProbeUrl(host: string, port: number): string | null {
if (!parsed.port && Number.isFinite(port) && port > 0) { if (!parsed.port && Number.isFinite(port) && port > 0) {
parsed.port = String(port) parsed.port = String(port)
} }
if (!parsed.pathname) parsed.pathname = '/' parsed.pathname = parsed.pathname.replace(/\/+$/, '') + '/api/health'
return parsed.toString() return parsed.toString()
} catch { } catch {
return null return null
@ -152,7 +152,7 @@ function buildGatewayProbeUrl(host: string, port: number): string | null {
} }
if (!Number.isFinite(port) || port <= 0) return null if (!Number.isFinite(port) || port <= 0) return null
return `http://${rawHost}:${port}/` return `http://${rawHost}:${port}/api/health`
} }
/** /**