Skip to content

Commit 8ad74b0

Browse files
committed
fix(gateway): contain connect error callbacks
1 parent 387fc93 commit 8ad74b0

2 files changed

Lines changed: 50 additions & 11 deletions

File tree

src/gateway/client.test.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,8 @@ function createClientWithIdentity(
253253
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
254254
const identity: DeviceIdentity = {
255255
deviceId,
256-
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
257-
publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString(),
256+
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }),
257+
publicKeyPem: publicKey.export({ type: "spki", format: "pem" }),
258258
};
259259
return new GatewayClient({
260260
url: "ws://127.0.0.1:18789",
@@ -1010,6 +1010,35 @@ describe("GatewayClient connect auth payload", () => {
10101010
}
10111011
});
10121012

1013+
it("keeps connect error callback throws inside challenge dispatch", () => {
1014+
const onConnectError = vi.fn(() => {
1015+
throw new Error("connect callback failed");
1016+
});
1017+
const client = new GatewayClient({
1018+
url: "ws://127.0.0.1:18789",
1019+
deviceIdentity: null,
1020+
onConnectError,
1021+
});
1022+
1023+
try {
1024+
client.start();
1025+
const ws = getLatestWs();
1026+
ws.emitOpen();
1027+
1028+
expect(() => emitConnectChallenge(ws, " ")).not.toThrow();
1029+
expect(onConnectError).toHaveBeenCalledOnce();
1030+
expect(ws.lastClose).toEqual({
1031+
code: 1008,
1032+
reason: "connect challenge missing nonce",
1033+
});
1034+
expect(logDebugMock).toHaveBeenCalledWith(
1035+
"gateway client connect error handler error: Error: connect callback failed",
1036+
);
1037+
} finally {
1038+
client.stop();
1039+
}
1040+
});
1041+
10131042
function emitConnectFailure(
10141043
ws: MockWebSocket,
10151044
connectId: string | undefined,

src/gateway/client.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ export class GatewayClient {
292292
this.connectSent = false;
293293
const url = this.opts.url ?? DEFAULT_GATEWAY_CLIENT_URL;
294294
if (this.opts.tlsFingerprint && !url.startsWith("wss://")) {
295-
this.opts.onConnectError?.(new Error("gateway tls fingerprint requires wss:// gateway url"));
295+
this.notifyConnectError(new Error("gateway tls fingerprint requires wss:// gateway url"));
296296
return;
297297
}
298298

@@ -318,7 +318,7 @@ export class GatewayClient {
318318
: "Break-glass (trusted private networks only): set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1. ") +
319319
"Run `openclaw doctor --fix` for guidance.",
320320
);
321-
this.opts.onConnectError?.(error);
321+
this.notifyConnectError(error);
322322
return;
323323
}
324324
// Allow node screen snapshots and other large responses.
@@ -367,7 +367,7 @@ export class GatewayClient {
367367
if (url.startsWith("wss://") && this.opts.tlsFingerprint) {
368368
const tlsError = this.validateTlsFingerprint();
369369
if (tlsError) {
370-
this.opts.onConnectError?.(tlsError);
370+
this.notifyConnectError(tlsError);
371371
this.ws?.close(1008, tlsError.message);
372372
return;
373373
}
@@ -432,7 +432,7 @@ export class GatewayClient {
432432
ws.on("error", (err) => {
433433
logDebug(`gateway client error: ${formatGatewayClientErrorForLog(err)}`);
434434
if (!this.connectSent) {
435-
this.opts.onConnectError?.(err instanceof Error ? err : new Error(String(err)));
435+
this.notifyConnectError(err instanceof Error ? err : new Error(String(err)));
436436
}
437437
});
438438
}
@@ -533,7 +533,7 @@ export class GatewayClient {
533533
}
534534
const nonce = normalizeOptionalString(this.connectNonce) ?? "";
535535
if (!nonce) {
536-
this.opts.onConnectError?.(new Error("gateway connect challenge missing nonce"));
536+
this.notifyConnectError(new Error("gateway connect challenge missing nonce"));
537537
this.ws?.close(1008, "connect challenge missing nonce");
538538
return;
539539
}
@@ -719,7 +719,7 @@ export class GatewayClient {
719719
this.ws?.close(1008, "connect retry");
720720
return;
721721
}
722-
this.opts.onConnectError?.(err instanceof Error ? err : new Error(String(err)));
722+
this.notifyConnectError(err instanceof Error ? err : new Error(String(err)));
723723
const msg = `gateway connect failed: ${formatGatewayClientErrorForLog(err)}`;
724724
if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE || isGatewayClientStoppedError(err)) {
725725
logDebug(msg);
@@ -734,7 +734,7 @@ export class GatewayClient {
734734
const error = err instanceof Error ? err : new Error(String(err));
735735
this.clearConnectChallengeTimeout();
736736
this.closed = true;
737-
this.opts.onConnectError?.(markGatewayConnectAssemblyError(error));
737+
this.notifyConnectError(markGatewayConnectAssemblyError(error));
738738
const msg = `gateway connect failed: ${formatGatewayClientErrorForLog(error)}`;
739739
if (this.opts.mode === GATEWAY_CLIENT_MODES.PROBE || isGatewayClientStoppedError(error)) {
740740
logDebug(msg);
@@ -744,6 +744,16 @@ export class GatewayClient {
744744
this.ws?.close(1008, "connect failed");
745745
}
746746

747+
private notifyConnectError(error: Error) {
748+
try {
749+
this.opts.onConnectError?.(error);
750+
} catch (err) {
751+
logDebug(
752+
`gateway client connect error handler error: ${formatGatewayClientErrorForLog(err)}`,
753+
);
754+
}
755+
}
756+
747757
private resolveConnectScopes(params: {
748758
usingStoredDeviceToken?: boolean;
749759
storedScopes?: string[];
@@ -953,7 +963,7 @@ export class GatewayClient {
953963
const payload = evt.payload as { nonce?: unknown } | undefined;
954964
const nonce = payload && typeof payload.nonce === "string" ? payload.nonce : null;
955965
if (!nonce || nonce.trim().length === 0) {
956-
this.opts.onConnectError?.(new Error("gateway connect challenge missing nonce"));
966+
this.notifyConnectError(new Error("gateway connect challenge missing nonce"));
957967
this.ws?.close(1008, "connect challenge missing nonce");
958968
return;
959969
}
@@ -1047,7 +1057,7 @@ export class GatewayClient {
10471057
return;
10481058
}
10491059
const elapsedMs = Date.now() - armedAt;
1050-
this.opts.onConnectError?.(
1060+
this.notifyConnectError(
10511061
new Error(
10521062
`gateway connect challenge timeout (waited ${elapsedMs}ms, limit ${connectChallengeTimeoutMs}ms)`,
10531063
),

0 commit comments

Comments
 (0)