fix: null-safe kRPC handshake error reporting
CI / Lint, typecheck, test, build (pull_request) Failing after 11s
CI / Lint, typecheck, test, build (pull_request) Failing after 11s
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.
This commit is contained in:
@@ -60,7 +60,8 @@ async function main(): Promise<void> {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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<KRPCClientOptions>;
|
||||
private rpcSocket: net.Socket | null = null;
|
||||
@@ -59,11 +85,17 @@ export class KRPCClient {
|
||||
|
||||
async connect(): Promise<void> {
|
||||
// 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,
|
||||
|
||||
Reference in New Issue
Block a user