From 7e2c4d3835ef528522eff21725448570a7f6cffd Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:00:43 +0800 Subject: [PATCH] fix: Host API port conflict crashing startup on Windows (#743) --- electron/api/server.ts | 14 +++++++++++++- electron/utils/config.ts | 2 +- src/lib/host-api.ts | 2 +- tests/unit/app-routes.test.ts | 4 ++-- tests/unit/channel-routes.test.ts | 14 +++++++------- tests/unit/cron-routes.test.ts | 8 ++++---- tests/unit/host-api.test.ts | 4 ++-- tests/unit/usage-routes.test.ts | 4 ++-- 8 files changed, 32 insertions(+), 20 deletions(-) diff --git a/electron/api/server.ts b/electron/api/server.ts index aca42e750..a6af24683 100644 --- a/electron/api/server.ts +++ b/electron/api/server.ts @@ -43,7 +43,7 @@ const routeHandlers: RouteHandler[] = [ * Per-session secret token used to authenticate Host API requests. * Generated once at server start and shared with the renderer via IPC. * This prevents cross-origin attackers from reading sensitive data even - * if they can reach 127.0.0.1:3210 (the CORS wildcard alone is not + * if they can reach 127.0.0.1:13210 (the CORS wildcard alone is not * sufficient because browsers attach the Origin header but not a secret). */ let hostApiToken: string = ''; @@ -106,6 +106,18 @@ export function startHostApiServer(ctx: HostApiContext, port = getPort('CLAWX_HO } }); + server.on('error', (error: NodeJS.ErrnoException) => { + if (error.code === 'EACCES' || error.code === 'EADDRINUSE') { + logger.error( + `Host API server failed to bind port ${port}: ${error.message}. ` + + 'On Windows this is often caused by Hyper-V reserving the port range. ' + + `Set CLAWX_PORT_CLAWX_HOST_API env var to override the default port.`, + ); + } else { + logger.error('Host API server error:', error); + } + }); + server.listen(port, '127.0.0.1', () => { logger.info(`Host API server listening on http://127.0.0.1:${port}`); }); diff --git a/electron/utils/config.ts b/electron/utils/config.ts index d221c972e..cd7817208 100644 --- a/electron/utils/config.ts +++ b/electron/utils/config.ts @@ -14,7 +14,7 @@ export const PORTS = { CLAWX_GUI: 23333, /** Local host API server port */ - CLAWX_HOST_API: 3210, + CLAWX_HOST_API: 13210, /** OpenClaw Gateway port */ OPENCLAW_GATEWAY: 18789, diff --git a/src/lib/host-api.ts b/src/lib/host-api.ts index 2f3e5e689..1ffd7887e 100644 --- a/src/lib/host-api.ts +++ b/src/lib/host-api.ts @@ -2,7 +2,7 @@ import { invokeIpc } from '@/lib/api-client'; import { trackUiEvent } from './telemetry'; import { normalizeAppError } from './error-model'; -const HOST_API_PORT = 3210; +const HOST_API_PORT = 13210; const HOST_API_BASE = `http://127.0.0.1:${HOST_API_PORT}`; /** Cached Host API auth token, fetched once from the main process via IPC. */ diff --git a/tests/unit/app-routes.test.ts b/tests/unit/app-routes.test.ts index 0cdfdeccd..7f112e657 100644 --- a/tests/unit/app-routes.test.ts +++ b/tests/unit/app-routes.test.ts @@ -30,7 +30,7 @@ describe('handleAppRoutes', () => { const handled = await handleAppRoutes( { method: 'POST' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/app/openclaw-doctor'), + new URL('http://127.0.0.1:13210/api/app/openclaw-doctor'), {} as never, ); @@ -48,7 +48,7 @@ describe('handleAppRoutes', () => { const handled = await handleAppRoutes( { method: 'POST' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/app/openclaw-doctor'), + new URL('http://127.0.0.1:13210/api/app/openclaw-doctor'), {} as never, ); diff --git a/tests/unit/channel-routes.test.ts b/tests/unit/channel-routes.test.ts index a67c17ee8..349b3c47c 100644 --- a/tests/unit/channel-routes.test.ts +++ b/tests/unit/channel-routes.test.ts @@ -159,7 +159,7 @@ describe('handleChannelRoutes', () => { const handled = await handleChannelRoutes( { method: 'GET' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/channels/accounts'), + new URL('http://127.0.0.1:13210/api/channels/accounts'), { gatewayManager: { rpc, @@ -241,7 +241,7 @@ describe('handleChannelRoutes', () => { await handleChannelRoutes( { method: 'GET' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/channels/accounts'), + new URL('http://127.0.0.1:13210/api/channels/accounts'), { gatewayManager: { rpc, @@ -296,7 +296,7 @@ describe('handleChannelRoutes', () => { const handled = await handleChannelRoutes( { method: 'GET' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/channels/targets?channelType=qqbot&accountId=default'), + new URL('http://127.0.0.1:13210/api/channels/targets?channelType=qqbot&accountId=default'), { gatewayManager: { rpc: vi.fn(), @@ -410,7 +410,7 @@ describe('handleChannelRoutes', () => { const handled = await handleChannelRoutes( { method: 'GET' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/channels/targets?channelType=feishu&accountId=default'), + new URL('http://127.0.0.1:13210/api/channels/targets?channelType=feishu&accountId=default'), { gatewayManager: { rpc: vi.fn(), @@ -469,7 +469,7 @@ describe('handleChannelRoutes', () => { const handled = await handleChannelRoutes( { method: 'GET' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/channels/targets?channelType=wecom&accountId=default'), + new URL('http://127.0.0.1:13210/api/channels/targets?channelType=wecom&accountId=default'), { gatewayManager: { rpc: vi.fn(), @@ -519,7 +519,7 @@ describe('handleChannelRoutes', () => { const handled = await handleChannelRoutes( { method: 'GET' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/channels/targets?channelType=dingtalk&accountId=default'), + new URL('http://127.0.0.1:13210/api/channels/targets?channelType=dingtalk&accountId=default'), { gatewayManager: { rpc: vi.fn(), @@ -571,7 +571,7 @@ describe('handleChannelRoutes', () => { const handled = await handleChannelRoutes( { method: 'GET' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/channels/targets?channelType=wechat&accountId=wechat-bot'), + new URL('http://127.0.0.1:13210/api/channels/targets?channelType=wechat&accountId=wechat-bot'), { gatewayManager: { rpc: vi.fn(), diff --git a/tests/unit/cron-routes.test.ts b/tests/unit/cron-routes.test.ts index 635bc52a4..c614da9fe 100644 --- a/tests/unit/cron-routes.test.ts +++ b/tests/unit/cron-routes.test.ts @@ -43,7 +43,7 @@ describe('handleCronRoutes', () => { const handled = await handleCronRoutes( { method: 'POST' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/cron/jobs'), + new URL('http://127.0.0.1:13210/api/cron/jobs'), { gatewayManager: { rpc }, } as never, @@ -89,7 +89,7 @@ describe('handleCronRoutes', () => { await handleCronRoutes( { method: 'PUT' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/cron/jobs/job-2'), + new URL('http://127.0.0.1:13210/api/cron/jobs/job-2'), { gatewayManager: { rpc }, } as never, @@ -139,7 +139,7 @@ describe('handleCronRoutes', () => { await handleCronRoutes( { method: 'PUT' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/cron/jobs/job-account'), + new URL('http://127.0.0.1:13210/api/cron/jobs/job-account'), { gatewayManager: { rpc }, } as never, @@ -178,7 +178,7 @@ describe('handleCronRoutes', () => { const handled = await handleCronRoutes( { method: 'POST' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/cron/jobs'), + new URL('http://127.0.0.1:13210/api/cron/jobs'), { gatewayManager: { rpc }, } as never, diff --git a/tests/unit/host-api.test.ts b/tests/unit/host-api.test.ts index 916bbff2d..9d45d513d 100644 --- a/tests/unit/host-api.test.ts +++ b/tests/unit/host-api.test.ts @@ -64,7 +64,7 @@ describe('host-api', () => { expect(result.fallback).toBe(true); expect(fetchMock).toHaveBeenCalledWith( - 'http://127.0.0.1:3210/api/test', + 'http://127.0.0.1:13210/api/test', expect.objectContaining({ headers: expect.any(Object) }), ); }); @@ -97,7 +97,7 @@ describe('host-api', () => { expect(result.fallback).toBe(true); expect(fetchMock).toHaveBeenCalledWith( - 'http://127.0.0.1:3210/api/test', + 'http://127.0.0.1:13210/api/test', expect.objectContaining({ headers: expect.any(Object) }), ); }); diff --git a/tests/unit/usage-routes.test.ts b/tests/unit/usage-routes.test.ts index 9e27e9810..0d8604639 100644 --- a/tests/unit/usage-routes.test.ts +++ b/tests/unit/usage-routes.test.ts @@ -24,7 +24,7 @@ describe('handleUsageRoutes', () => { const handled = await handleUsageRoutes( { method: 'GET' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/usage/recent-token-history'), + new URL('http://127.0.0.1:13210/api/usage/recent-token-history'), {} as never, ); @@ -44,7 +44,7 @@ describe('handleUsageRoutes', () => { await handleUsageRoutes( { method: 'GET' } as IncomingMessage, {} as ServerResponse, - new URL('http://127.0.0.1:3210/api/usage/recent-token-history?limit=50.9'), + new URL('http://127.0.0.1:13210/api/usage/recent-token-history?limit=50.9'), {} as never, );