diff --git a/src/gateway/server/ws-connection.test.ts b/src/gateway/server/ws-connection.test.ts index 30f877b3800f..9f558a0b2210 100644 --- a/src/gateway/server/ws-connection.test.ts +++ b/src/gateway/server/ws-connection.test.ts @@ -283,6 +283,46 @@ describe("attachGatewayWsConnectionHandler", () => { expect(context).toMatchObject({ phase: "ws_upgrade_started" }); }); + it("demotes local app startup aborts before the first frame", async () => { + const { socket, logWsControl } = await connectTestWs({ + headers: { "user-agent": "OpenClaw/2607000290 CFNetwork/3860 Darwin/25" }, + options: { isStartupPending: () => true }, + }); + + socket.emit("close", 1006, Buffer.alloc(0)); + + expect(logWsControl.debug).toHaveBeenCalledWith( + expect.stringContaining("closed before connect"), + expect.objectContaining({ phase: "ws_upgrade_started" }), + ); + expect(logWsControl.warn).not.toHaveBeenCalledWith( + expect.stringContaining("closed before connect"), + expect.anything(), + ); + }); + + it("keeps queued local app startup frames at warning level", async () => { + const logWsControl = createGatewayWsTestLogger(); + const { socket } = attachGatewayWsForTest({ + attach: attachGatewayWsConnectionHandler, + headers: { "user-agent": "OpenClaw/2607000290 CFNetwork/3860 Darwin/25" }, + options: { isStartupPending: () => true, logWsControl: logWsControl as never }, + }); + + socket.emit("message", Buffer.from('{"type":"req","id":"queued"}')); + socket.emit("close", 1006, Buffer.alloc(0)); + await waitForLazyMessageHandler(); + + expect(logWsControl.warn).toHaveBeenCalledWith( + expect.stringContaining("closed before connect"), + expect.objectContaining({ phase: "ws_upgrade_started" }), + ); + expect(logWsControl.debug).not.toHaveBeenCalledWith( + expect.stringContaining("closed before connect"), + expect.anything(), + ); + }); + it("includes the last completed handshake phase on preauth timeout logs", async () => { vi.useFakeTimers(); const { logWsControl } = await connectTestWs({ diff --git a/src/gateway/server/ws-connection.ts b/src/gateway/server/ws-connection.ts index c54d5129314a..6f9cbf498ec6 100644 --- a/src/gateway/server/ws-connection.ts +++ b/src/gateway/server/ws-connection.ts @@ -273,6 +273,7 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti const requestUserAgent = headerValue(upgradeReq.headers["user-agent"]); const forwardedFor = headerValue(upgradeReq.headers["x-forwarded-for"]); const realIp = headerValue(upgradeReq.headers["x-real-ip"]); + const openedDuringStartup = isStartupPending?.() === true; const pluginNodeCapabilities = getPluginNodeCapabilities?.() ?? []; const pluginSurfaceBaseUrl = @@ -296,6 +297,11 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti let lastFrameType: string | undefined; let lastFrameMethod: string | undefined; let lastFrameId: string | undefined; + let hasReceivedPreauthFrame = false; + + socket.once("message", () => { + hasReceivedPreauthFrame = true; + }); const advanceHandshakePhase = (next: WsHandshakePhase) => { if (WS_HANDSHAKE_PHASES.indexOf(next) > WS_HANDSHAKE_PHASES.indexOf(lastHandshakePhase)) { @@ -421,6 +427,15 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti normalizeLowercaseStringOrEmpty(userAgent).includes("swiftpm-testing-helper") && isLoopbackAddress(remote); + const isExpectedLocalAppStartupAbort = (code: number) => + openedDuringStartup && + code === 1006 && + lastHandshakePhase === "ws_upgrade_started" && + !hasReceivedPreauthFrame && + lastFrameType === undefined && + normalizeLowercaseStringOrEmpty(requestUserAgent).startsWith("openclaw/") && + isLoopbackAddress(remoteAddr); + socket.once("close", (code, reason) => { const durationMs = Date.now() - openedAt; const logForwardedFor = sanitizeLogValue(forwardedFor); @@ -452,7 +467,9 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti const isExpectedStartupRetryClose = closeCause === GATEWAY_STARTUP_PENDING_CLOSE_CAUSE && code === GATEWAY_STARTUP_CLOSE_CODE; const logFn = - isNoisySwiftPmHelperClose(requestUserAgent, remoteAddr) || isExpectedStartupRetryClose + isNoisySwiftPmHelperClose(requestUserAgent, remoteAddr) || + isExpectedStartupRetryClose || + isExpectedLocalAppStartupAbort(code) ? logWsControl.debug : logWsControl.warn; const authReason = stringMetaValue(closeMeta, "authReason");