From a69cd148171feac525be5e4aff8a85567c8d72a8 Mon Sep 17 00:00:00 2001 From: Mavis Date: Tue, 2 Jun 2026 23:10:19 +0000 Subject: [PATCH] fix: null-safe kRPC handshake error reporting The bridge was falling back to mock mode with a confusing 'Cannot read properties of null (reading code)' error. The actual underlying error (ECONNREFUSED, timeout, protocol mismatch) was being swallowed by our error handler that did `(e as Error).message` on a value that was sometimes null. Wrap the kRPC client connect() in per-step try/catch with a formatErr() helper that handles: - null / undefined - strings - Error with .code (NodeJS.ErrnoException) - arbitrary objects (JSON.stringify fallback) Now when the bridge can't reach kRPC you get a real error like 'kRPC RPC TCP connect to 127.0.0.1:50000 failed: code=ECONNREFUSED: connect ECONNREFUSED 127.0.0.1:50000' instead of the cryptic null-code message. Also fixed the bridge's main() error handler to be null-safe. Discovered while debugging the user's first end-to-end run on Windows: kRPC was reachable (Test-NetConnection succeeded) but the bridge couldn't complete the handshake. With this fix we'll see the real failure mode on the next attempt. --- apps/tools/ksp-bridge/src/index.ts | 3 +- packages/krpc-client/src/client.ts | 73 ++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/apps/tools/ksp-bridge/src/index.ts b/apps/tools/ksp-bridge/src/index.ts index 2343fff..557ff6f 100644 --- a/apps/tools/ksp-bridge/src/index.ts +++ b/apps/tools/ksp-bridge/src/index.ts @@ -60,7 +60,8 @@ async function main(): Promise { await adapter.connect(); log(`connected to kRPC at ${HOST}:${RPC_PORT} — running with real KSP state`); } catch (e) { - log(`no kRPC server at ${HOST}:${RPC_PORT}: ${(e as Error).message}`); + const msg = e === null ? 'null' : e instanceof Error ? e.message : String(e); + log(`no kRPC server at ${HOST}:${RPC_PORT}: ${msg}`); log('Falling back to MOCK mode (synthetic state).'); return runMock(); } diff --git a/packages/krpc-client/src/client.ts b/packages/krpc-client/src/client.ts index 7a1f5ab..3a8525d 100644 --- a/packages/krpc-client/src/client.ts +++ b/packages/krpc-client/src/client.ts @@ -38,6 +38,32 @@ export interface ProcedureCallRequest { type StreamHandler = (streamId: number, result: Uint8Array) => void; export type { StreamHandler }; +/** + * Format an error value for human consumption. Handles the cases where + * the thrown value is null, undefined, a string, or an Error with + * .code (NodeJS.ErrnoException). Falls back to JSON.stringify for + * unknown shapes. + */ +function formatErr(e: unknown): string { + if (e === null) return 'null'; + if (e === undefined) return 'undefined'; + if (typeof e === 'string') return e; + if (typeof e === 'object') { + const obj = e as { code?: unknown; message?: unknown; errno?: unknown }; + const parts: string[] = []; + if (typeof obj.code === 'string') parts.push(`code=${obj.code}`); + if (typeof obj.errno === 'number') parts.push(`errno=${obj.errno}`); + if (typeof obj.message === 'string') parts.push(obj.message); + if (parts.length > 0) return parts.join(': '); + try { + return JSON.stringify(e); + } catch { + return String(e); + } + } + return String(e); +} + export class KRPCClient { private opts: Required; private rpcSocket: net.Socket | null = null; @@ -59,11 +85,17 @@ export class KRPCClient { async connect(): Promise { // RPC handshake - this.rpcSocket = await tcpConnect( - this.opts.host, - this.opts.rpcPort, - this.opts.connectTimeoutMs, - ); + try { + this.rpcSocket = await tcpConnect( + this.opts.host, + this.opts.rpcPort, + this.opts.connectTimeoutMs, + ); + } catch (e) { + throw new Error( + `kRPC RPC TCP connect to ${this.opts.host}:${this.opts.rpcPort} failed: ${formatErr(e)}`, + ); + } // The ConnectionRequest.Type enum has RPC = 0, STREAM = 1. We pass // the numeric value directly because the nested-enum name lookup // is brittle across protobufjs versions when the enum is nested @@ -72,11 +104,16 @@ export class KRPCClient { type: 0, // RPC clientName: this.opts.clientName, }); - const resp = decodeMessage<{ - status: number | string; - message: string; - clientIdentifier: Uint8Array; - }>(KRPC.ConnectionResponse, await recvRawMessage(this.rpcSocket)); + let resp: { status: number | string; message: string; clientIdentifier: Uint8Array }; + try { + resp = decodeMessage<{ + status: number | string; + message: string; + clientIdentifier: Uint8Array; + }>(KRPC.ConnectionResponse, await recvRawMessage(this.rpcSocket)); + } catch (e) { + throw new Error(`kRPC RPC handshake (response decode) failed: ${formatErr(e)}`); + } // protobufjs decodes enums to numbers by default; OK == 0 if (resp.status !== 'OK' && resp.status !== 0) { throw new Error(`RPC handshake failed: ${resp.status} ${resp.message}`); @@ -84,11 +121,17 @@ export class KRPCClient { this.clientIdentifier = Buffer.from(resp.clientIdentifier); // Stream handshake - this.streamSocket = await tcpConnect( - this.opts.host, - this.opts.streamPort, - this.opts.connectTimeoutMs, - ); + try { + this.streamSocket = await tcpConnect( + this.opts.host, + this.opts.streamPort, + this.opts.connectTimeoutMs, + ); + } catch (e) { + throw new Error( + `kRPC Stream TCP connect to ${this.opts.host}:${this.opts.streamPort} failed: ${formatErr(e)}`, + ); + } sendMessage(this.streamSocket, KRPC.ConnectionRequest, { type: 1, // STREAM clientIdentifier: this.clientIdentifier,