From 639d265278565f059e0275438e73fe4d995b9862 Mon Sep 17 00:00:00 2001 From: Mavis Date: Wed, 3 Jun 2026 00:15:00 +0000 Subject: [PATCH] fix: decode error bytes as Error protobuf (not as a pre-decoded object) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kRPC Response and ProcedureResult messages use `bytes` for their `error` fields — a serialized Error sub-message, not a pre-decoded object. We were treating them as already-decoded, which produced 'RPC error: .:`\u2426' (empty service.name.description with a stray substitute character) and triggered 'invalid wire type 4 / 7' errors when the bytes happened to look like other protobuf structures. Same bug existed in three places: 1. invoke() - top-level Response.error and per-call results[i].error 2. addStream() - results[i].error 3. readStreamLoop() - StreamResult.result.error Fix: when an error field is set and non-empty, decode the bytes as a kRPC.Error message to get service/name/description/stackTrace. If the inner decode fails, fall back to a hex dump so we still see something useful in the logs. Stream errors are logged as warnings and produce empty values so the stream loop keeps running for the other streams. --- packages/krpc-client/src/client.ts | 63 ++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/krpc-client/src/client.ts b/packages/krpc-client/src/client.ts index 451eeb7..f3e369c 100644 --- a/packages/krpc-client/src/client.ts +++ b/packages/krpc-client/src/client.ts @@ -226,15 +226,22 @@ export class KRPCClient { sendMessage(this.rpcSocket, KRPC.Request, { calls: [call] }); const raw = await recvRawMessage(this.rpcSocket); const response = decodeMessage<{ - error?: { service: string; name: string; description: string }; - results: { - error?: { service: string; name: string; description: string }; - value: Uint8Array; - }[]; + // The error/result fields are `bytes` in the kRPC schema. They + // carry a serialized Error or value sub-message that we have + // to decode on its own — not a pre-decoded object. + error?: Uint8Array; + results: { error?: Uint8Array; value: Uint8Array }[]; }>(KRPC.Response, raw); - if (response.error) { + if (response.error && response.error.length > 0) { + const err = decodeMessage<{ + service: string; + name: string; + description: string; + stackTrace: string; + }>(KRPC.Error, response.error); throw new Error( - `RPC error: ${response.error.service}.${response.error.name}: ${response.error.description}`, + `RPC error: ${err.service}.${err.name}: ${err.description}` + + (err.stackTrace ? `\n${err.stackTrace}` : ''), ); } if (response.results.length === 0) { @@ -244,9 +251,16 @@ export class KRPCClient { if (!r) { throw new Error('empty response result'); } - if (r.error) { + if (r.error && r.error.length > 0) { + const err = decodeMessage<{ + service: string; + name: string; + description: string; + stackTrace: string; + }>(KRPC.Error, r.error); throw new Error( - `RPC result error: ${r.error.service}.${r.error.name}: ${r.error.description}`, + `RPC result error: ${err.service}.${err.name}: ${err.description}` + + (err.stackTrace ? `\n${err.stackTrace}` : ''), ); } return r.value; @@ -276,13 +290,21 @@ export class KRPCClient { }; sendMessage(this.rpcSocket, KRPC.Request, { calls: [addCall] }); const response = decodeMessage<{ - results: { value: Uint8Array; error?: { name: string; description: string } }[]; + results: { value: Uint8Array; error?: Uint8Array }[]; }>(KRPC.Response, await recvRawMessage(this.rpcSocket)); if (response.results.length === 0) throw new Error('empty AddStream response'); const r0 = response.results[0]; if (!r0) throw new Error('empty AddStream result'); - if (r0.error) { - throw new Error(`AddStream error: ${r0.error.name}: ${r0.error.description}`); + if (r0.error && r0.error.length > 0) { + const err = decodeMessage<{ + service: string; + name: string; + description: string; + stackTrace: string; + }>(KRPC.Error, r0.error); + throw new Error( + `AddStream error: ${err.service}.${err.name}: ${err.description}`, + ); } const stream = decodeMessage<{ id: number }>(KRPC.Stream, r0.value); return stream.id; @@ -330,9 +352,24 @@ export class KRPCClient { try { const raw = await recvRawMessage(this.streamSocket); const update = decodeMessage<{ - results: { id: number; result: { value: Uint8Array } }[]; + results: { id: number; result: { error?: Uint8Array; value: Uint8Array } }[]; }>(KRPC.StreamUpdate, raw); for (const r of update.results) { + if (r.result.error && r.result.error.length > 0) { + const err = decodeMessage<{ + service: string; + name: string; + description: string; + stackTrace: string; + }>(KRPC.Error, r.result.error); + // eslint-disable-next-line no-console + console.warn( + `[krpc-client] stream ${r.id} error: ${err.service}.${err.name}: ${err.description}`, + ); + // Pass empty value so the stream handler sees "no data" rather than crashing + for (const h of this.streamHandlers) h(r.id, new Uint8Array()); + continue; + } for (const h of this.streamHandlers) h(r.id, r.result.value); } } catch (err) {