Skip to content

Commit 517f6b2

Browse files
committed
fix: fallback to structured-clone for RPC error envelopes
1 parent e5b8fca commit 517f6b2

3 files changed

Lines changed: 46 additions & 2 deletions

File tree

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,12 @@ 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` constrains the return-value path (args + return).
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+
// Detect via `'e' in msg` so `throw undefined` still routes through SC.
91+
const isErrorResponse = msg.t === 's' && 'e' in msg
92+
const useJson = !isErrorResponse && !!method && definitions.get(method)?.jsonSerializable === true
8893
if (useJson)
8994
return strictJsonStringify(msg, method ?? '')
9095
return `${STRUCTURED_CLONE_PREFIX}${structuredCloneStringify(msg)}`

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,12 @@ 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` constrains the return-value path (args + return).
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+
// Detect via `'e' in msg` so `throw undefined` still routes through SC.
134+
const isErrorResponse = msg.t === 's' && 'e' in msg
135+
const useJson = !isErrorResponse && !!method && definitions.get(method)?.jsonSerializable === true
131136
if (useJson)
132137
return strictJsonStringify(msg, method ?? '')
133138
return `${STRUCTURED_CLONE_PREFIX}${structuredCloneStringify(msg)}`

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,38 @@ 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+
const { wss } = attachWsRpcTransport(server, { port: PORT, host: HOST, definitions: definitions as any })
76+
77+
try {
78+
const client = createRpcClient<typeof serverFunctions, Record<string, never>>({}, {
79+
channel: createWsRpcChannel({ url: WS_URL, definitions: definitions as any }),
80+
})
81+
82+
await expect(client.$call('explode')).rejects.toThrow(/boom/)
83+
}
84+
finally {
85+
for (const c of wss.clients) c.terminate()
86+
await new Promise<void>(resolve => wss.close(() => resolve()))
87+
}
88+
})
5589
})

0 commit comments

Comments
 (0)