fix: handle null returnType in decodeKrpcType (the actual root cause!)

The stack trace finally showed the truth: the error was in our
own decodeKrpcType, not in protobufjs. The kRPC server omits
the `return_type` field for procedures that have no return value
(e.g. AddStream, RemoveStream, all the setters). protobufjs
decodes missing message fields as null. Our cache then called
`decodeKrpcType(null)`, which did `null.code` and threw.

The null.code error message was a red herring all along — it
looked like protobufjs was looking up an enum descriptor, but it
was actually just our code accessing .code on a null parameter.

The protobufjs fixes (uint32 instead of nested-enum, adding the
game_scenes field) were real and needed — but they weren't the
cause of THIS particular failure mode.

Fix:
- decodeKrpcType accepts null/undefined and returns a NONE type
  (code 0) in that case
- Added a regression test in services.test.ts that builds a fake
  ServiceCache with a procedure whose returnType is null
This commit is contained in:
Mavis
2026-06-02 23:56:32 +00:00
parent 2b0573d328
commit 62e7ed0a77
2 changed files with 46 additions and 1 deletions
+17 -1
View File
@@ -78,7 +78,23 @@ export interface RawKrpcTypeMessage {
types: RawKrpcTypeMessage[];
}
export function decodeKrpcType(raw: RawKrpcTypeMessage): KrpcType {
/**
* Decode a kRPC Type protobuf message into our plain KrpcType shape.
*
* Returns a NONE-type KrpcType if `raw` is null/undefined or doesn't
* have a `code` field — which happens for procedures with no return
* value (the kRPC server omits the `return_type` field). We treat that
* as the NONE type code (0) rather than throwing.
*/
export function decodeKrpcType(raw: RawKrpcTypeMessage | null | undefined): KrpcType {
if (!raw || typeof raw.code !== 'number') {
return {
code: 0 as TypeCodeValue, // NONE
service: '',
name: '',
types: [],
};
}
return {
code: raw.code as TypeCodeValue,
service: raw.service ?? '',
@@ -196,4 +196,33 @@ describe('ServiceCache', () => {
expect(r.info.returnType.types[0]?.code).toBe(100);
expect(r.info.returnType.types[0]?.name).toBe('CelestialBody');
});
it('handles procedures with no return type (returnType is null)', () => {
// Real kRPC server omits the `return_type` field for void
// procedures (e.g. AddStream, setters). protobufjs decodes
// missing message fields as null. Our cache must not crash.
const raw = {
services: [
{
name: 'KRPC',
procedures: [
{
name: 'AddStream',
parameters: [],
returnType: null, // <-- the trigger
returnIsNullable: false,
},
],
classes: [],
enumerations: [],
},
],
};
const cache = new ServiceCache(raw as unknown as Parameters<typeof ServiceCache>[0]);
const r = cache.lookup('KRPC', 'AddStream');
expect(r.found).toBe(true);
if (!r.found) throw new Error('unreachable');
// Missing return type becomes NONE (code 0).
expect(r.info.returnType.code).toBe(0);
});
});