Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/devframe/src/rpc/transports/ws-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ export function createWsRpcChannel(options: WsRpcChannelOptions): ChannelOptions
method = pendingRequestMethods.get(msg.i)
pendingRequestMethods.delete(msg.i)
}
const useJson = !!method && definitions.get(method)?.jsonSerializable === true
// `jsonSerializable` constrains the return-value path (args + return).
// Error envelopes (`{ t: 's', i, e }`) carry a thrown value — fall back
// to structured-clone so they round-trip instead of crashing the serializer.
// Detect via `'e' in msg` so `throw undefined` still routes through SC.
const isErrorResponse = msg.t === 's' && 'e' in msg
const useJson = !isErrorResponse && !!method && definitions.get(method)?.jsonSerializable === true
if (useJson)
return strictJsonStringify(msg, method ?? '')
return `${STRUCTURED_CLONE_PREFIX}${structuredCloneStringify(msg)}`
Expand Down
7 changes: 6 additions & 1 deletion packages/devframe/src/rpc/transports/ws-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,12 @@ export function attachWsRpcTransport<
method = pendingRequestMethods.get(msg.i)
pendingRequestMethods.delete(msg.i)
}
const useJson = !!method && definitions.get(method)?.jsonSerializable === true
// `jsonSerializable` constrains the return-value path (args + return).
// Error envelopes (`{ t: 's', i, e }`) carry a thrown value — fall back
// to structured-clone so they round-trip instead of crashing the serializer.
// Detect via `'e' in msg` so `throw undefined` still routes through SC.
const isErrorResponse = msg.t === 's' && 'e' in msg
const useJson = !isErrorResponse && !!method && definitions.get(method)?.jsonSerializable === true
if (useJson)
return strictJsonStringify(msg, method ?? '')
return `${STRUCTURED_CLONE_PREFIX}${structuredCloneStringify(msg)}`
Expand Down
34 changes: 34 additions & 0 deletions packages/devframe/src/rpc/transports/ws.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,38 @@ describe('devtools rpc', () => {

expect(await server.broadcast.$call('hey', 'server')).toEqual(expect.arrayContaining(['hey server, I\'m client 1', 'hey server, I\'m client 2']))
})

// Regression: a `jsonSerializable: true` RPC that throws used to crash the
// WS serializer with DF0020 because the error envelope was strict-JSON-encoded
// alongside the result path.
it('returns a rejection (not a serialization crash) when a jsonSerializable RPC throws', async () => {
const PORT = 3334
const HOST = '127.0.0.1'
const WS_URL = `ws://${HOST}:${PORT}`

const serverFunctions = {
explode: async () => {
throw new Error('boom')
},
}

const definitions = new Map<string, { jsonSerializable?: boolean }>([
['explode', { jsonSerializable: true }],
])

const server = createRpcServer<Record<string, never>, typeof serverFunctions>(serverFunctions)
const { wss } = attachWsRpcTransport(server, { port: PORT, host: HOST, definitions: definitions as any })

try {
const client = createRpcClient<typeof serverFunctions, Record<string, never>>({}, {
channel: createWsRpcChannel({ url: WS_URL, definitions: definitions as any }),
})

await expect(client.$call('explode')).rejects.toThrow(/boom/)
}
finally {
for (const c of wss.clients) c.terminate()
await new Promise<void>(resolve => wss.close(() => resolve()))
}
})
})
Loading