fix: decode error bytes as Error protobuf (not as a pre-decoded object)

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.
This commit is contained in:
Mavis
2026-06-03 00:15:00 +00:00
parent e9ebbf17d2
commit 639d265278
+50 -13
View File
@@ -226,15 +226,22 @@ export class KRPCClient {
sendMessage(this.rpcSocket, KRPC.Request, { calls: [call] }); sendMessage(this.rpcSocket, KRPC.Request, { calls: [call] });
const raw = await recvRawMessage(this.rpcSocket); const raw = await recvRawMessage(this.rpcSocket);
const response = decodeMessage<{ const response = decodeMessage<{
error?: { service: string; name: string; description: string }; // The error/result fields are `bytes` in the kRPC schema. They
results: { // carry a serialized Error or value sub-message that we have
error?: { service: string; name: string; description: string }; // to decode on its own — not a pre-decoded object.
value: Uint8Array; error?: Uint8Array;
}[]; results: { error?: Uint8Array; value: Uint8Array }[];
}>(KRPC.Response, raw); }>(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( 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) { if (response.results.length === 0) {
@@ -244,9 +251,16 @@ export class KRPCClient {
if (!r) { if (!r) {
throw new Error('empty response result'); 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( 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; return r.value;
@@ -276,13 +290,21 @@ export class KRPCClient {
}; };
sendMessage(this.rpcSocket, KRPC.Request, { calls: [addCall] }); sendMessage(this.rpcSocket, KRPC.Request, { calls: [addCall] });
const response = decodeMessage<{ const response = decodeMessage<{
results: { value: Uint8Array; error?: { name: string; description: string } }[]; results: { value: Uint8Array; error?: Uint8Array }[];
}>(KRPC.Response, await recvRawMessage(this.rpcSocket)); }>(KRPC.Response, await recvRawMessage(this.rpcSocket));
if (response.results.length === 0) throw new Error('empty AddStream response'); if (response.results.length === 0) throw new Error('empty AddStream response');
const r0 = response.results[0]; const r0 = response.results[0];
if (!r0) throw new Error('empty AddStream result'); if (!r0) throw new Error('empty AddStream result');
if (r0.error) { if (r0.error && r0.error.length > 0) {
throw new Error(`AddStream error: ${r0.error.name}: ${r0.error.description}`); 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); const stream = decodeMessage<{ id: number }>(KRPC.Stream, r0.value);
return stream.id; return stream.id;
@@ -330,9 +352,24 @@ export class KRPCClient {
try { try {
const raw = await recvRawMessage(this.streamSocket); const raw = await recvRawMessage(this.streamSocket);
const update = decodeMessage<{ const update = decodeMessage<{
results: { id: number; result: { value: Uint8Array } }[]; results: { id: number; result: { error?: Uint8Array; value: Uint8Array } }[];
}>(KRPC.StreamUpdate, raw); }>(KRPC.StreamUpdate, raw);
for (const r of update.results) { 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); for (const h of this.streamHandlers) h(r.id, r.result.value);
} }
} catch (err) { } catch (err) {