Skip to content

Commit d124efc

Browse files
georgiclaude
andcommitted
fix(protocol): accept a null trpcCode on rpc_response errors
MsgPack encodes an absent field as null, so a plain (non-tRPC) error reached the client with trpcCode: null while the schema required a string. Every provider failure logged a protocol violation next to the real error. trpcCode is now nullable, matching apiCode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CXRPKSQMTHjNBjrkdNMzUZ
1 parent 1a7eec1 commit d124efc

3 files changed

Lines changed: 54 additions & 2 deletions

File tree

packages/protocol/src/messages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ export interface RpcErrorPayload {
907907
message: string;
908908
retryable: boolean;
909909
apiCode?: string | null;
910-
trpcCode?: string;
910+
trpcCode?: string | null;
911911
}
912912

913913
/**

packages/protocol/src/ws-commands.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,8 @@ export const rpcErrorPayloadOutSchema = z
472472
message: z.string(),
473473
retryable: z.boolean(),
474474
apiCode: z.string().nullable().optional(),
475-
trpcCode: z.string().optional()
475+
// MsgPack encodes an absent trpcCode as null, so accept both.
476+
trpcCode: z.string().nullable().optional()
476477
})
477478
.passthrough();
478479

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, it, expect } from "vitest";
2+
import { outboundControlMessageSchemas } from "../src/ws-commands.js";
3+
4+
const schema = outboundControlMessageSchemas.rpc_response;
5+
6+
describe("rpc_response outbound schema", () => {
7+
// MsgPack encodes an absent field as null, so a plain (non-tRPC) error
8+
// reaches the client with trpcCode: null. Rejecting that logged a protocol
9+
// violation on every provider failure.
10+
it("accepts a null trpcCode", () => {
11+
const parsed = schema.safeParse({
12+
type: "rpc_response",
13+
request_id: "r1",
14+
command: "generate_media",
15+
error: {
16+
code: "INTERNAL_ERROR",
17+
message: "Unprocessable Entity",
18+
retryable: true,
19+
apiCode: null,
20+
trpcCode: null
21+
}
22+
});
23+
expect(parsed.success).toBe(true);
24+
});
25+
26+
it("accepts a string trpcCode", () => {
27+
const parsed = schema.safeParse({
28+
type: "rpc_response",
29+
request_id: "r1",
30+
command: "get_asset",
31+
error: {
32+
code: "NOT_FOUND",
33+
message: "missing",
34+
retryable: false,
35+
apiCode: "not_found",
36+
trpcCode: "NOT_FOUND"
37+
}
38+
});
39+
expect(parsed.success).toBe(true);
40+
});
41+
42+
it("rejects a non-string trpcCode", () => {
43+
const parsed = schema.safeParse({
44+
type: "rpc_response",
45+
request_id: "r1",
46+
command: "get_asset",
47+
error: { code: "X", message: "m", retryable: false, trpcCode: 7 }
48+
});
49+
expect(parsed.success).toBe(false);
50+
});
51+
});

0 commit comments

Comments
 (0)