Skip to content

Commit 3eef8b5

Browse files
committed
fix: fall back to structured-clone for RPC error envelopes
1 parent 18e7dab commit 3eef8b5

3 files changed

Lines changed: 38 additions & 2 deletions

File tree

devframe/packages/devframe/src/rpc/transports/ws-client.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ export function createWsRpcChannel(options: WsRpcChannelOptions): ChannelOptions
8484
method = pendingRequestMethods.get(msg.i)
8585
pendingRequestMethods.delete(msg.i)
8686
}
87-
const useJson = !!method && definitions.get(method)?.jsonSerializable === true
87+
// `jsonSerializable` describes the *result* shape, not the error path.
88+
// Error envelopes (`{ t: 's', i, e }`) carry a thrown value — fall back
89+
// to structured-clone so they round-trip instead of crashing the serializer.
90+
const isErrorResponse = msg.t !== 'q' && msg.e !== undefined
91+
const useJson = !isErrorResponse && !!method && definitions.get(method)?.jsonSerializable === true
8892
if (useJson)
8993
return strictJsonStringify(msg, method ?? '')
9094
return `${STRUCTURED_CLONE_PREFIX}${structuredCloneStringify(msg)}`

devframe/packages/devframe/src/rpc/transports/ws-server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ export function attachWsRpcTransport<
127127
method = pendingRequestMethods.get(msg.i)
128128
pendingRequestMethods.delete(msg.i)
129129
}
130-
const useJson = !!method && definitions.get(method)?.jsonSerializable === true
130+
// `jsonSerializable` describes the *result* shape, not the error path.
131+
// Error envelopes (`{ t: 's', i, e }`) carry a thrown value — fall back
132+
// to structured-clone so they round-trip instead of crashing the serializer.
133+
const isErrorResponse = msg.t !== 'q' && msg.e !== undefined
134+
const useJson = !isErrorResponse && !!method && definitions.get(method)?.jsonSerializable === true
131135
if (useJson)
132136
return strictJsonStringify(msg, method ?? '')
133137
return `${STRUCTURED_CLONE_PREFIX}${structuredCloneStringify(msg)}`

devframe/packages/devframe/src/rpc/transports/ws.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,32 @@ describe('devtools rpc', () => {
5252

5353
expect(await server.broadcast.$call('hey', 'server')).toEqual(expect.arrayContaining(['hey server, I\'m client 1', 'hey server, I\'m client 2']))
5454
})
55+
56+
// Regression: a `jsonSerializable: true` RPC that throws used to crash the
57+
// WS serializer with DF0020 because the error envelope was strict-JSON-encoded
58+
// alongside the result path.
59+
it('returns a rejection (not a serialization crash) when a jsonSerializable RPC throws', async () => {
60+
const PORT = 3334
61+
const HOST = '127.0.0.1'
62+
const WS_URL = `ws://${HOST}:${PORT}`
63+
64+
const serverFunctions = {
65+
explode: async () => {
66+
throw new Error('boom')
67+
},
68+
}
69+
70+
const definitions = new Map<string, { jsonSerializable?: boolean }>([
71+
['explode', { jsonSerializable: true }],
72+
])
73+
74+
const server = createRpcServer<Record<string, never>, typeof serverFunctions>(serverFunctions)
75+
attachWsRpcTransport(server, { port: PORT, host: HOST, definitions: definitions as any })
76+
77+
const client = createRpcClient<typeof serverFunctions, Record<string, never>>({}, {
78+
channel: createWsRpcChannel({ url: WS_URL, definitions: definitions as any }),
79+
})
80+
81+
await expect(client.$call('explode')).rejects.toThrow(/boom/)
82+
})
5583
})

0 commit comments

Comments
 (0)