Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions apps/content/docs/rpc/serializer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ const link = new RPCLink({

:::

:::info[Handlers out of sync]
Client and server can run different versions, so one side may receive a handler key it doesn't know. The value is then left in its serialized form instead of failing the whole payload, and values nested inside it are still restored. Configure the same handlers on both sides to get the original types back.
:::

## BigInt Digit Limit

The built-in `bigint` handler only accepts decimal strings of up to 4300 digits, since parsing longer ones can block the event loop. To allow longer values, override it on both sides:
Expand Down
21 changes: 19 additions & 2 deletions packages/client/src/rpc-json-serializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,24 @@ describe('rpcJsonSerializer: custom handlers', () => {
})
})

describe('rpcJsonSerializer: unknown types', () => {
const serializer = new RPCJsonSerializer({ handlers: { url: undefined } })

it('leaves their values in serialized form, still restoring known values inside', () => {
expect(serializer.deserializeJson({
json: { person: { name: 'dinwwwh', date: '2023-01-01T00:00:00.000Z' }, url: 'https://orpc.dev/' },
meta: [['date', 'person', 'date'], ['person', 'person'], ['url', 'url']],
})).toEqual({ person: { name: 'dinwwwh', date: new Date('2023-01-01') }, url: 'https://orpc.dev/' })
})

it('skips their entries without walking the path', () => {
expect(serializer.deserializeJson({
json: { a: { b: { x: '1', y: '2', z: '3' } }, v: '4' },
meta: [['bigint', 'a', 'b', 'x'], ['unknown', 'a', 'b', 'y'], ['bigint', 'a', 'b', 'z'], ['unknown', 'missing', 'deep'], ['bigint', 'v'], ['unknown', 'v', 'x']],
})).toEqual({ a: { b: { x: 1n, y: '2', z: 3n } }, v: 4n })
})
})

describe('rpcJsonSerializer: deserialize', () => {
const serializer = new RPCJsonSerializer()

Expand Down Expand Up @@ -455,8 +473,7 @@ describe('rpcJsonSerializer: security', () => {
})

it.each(['nonexistent', '__proto__', 'constructor', 'prototype', 'toString', 'hasOwnProperty', 'valueOf'])('never resolves the meta type "%s" through the prototype chain', (type) => {
expect(() => serializer.deserializeJson({ json: 1, meta: [[type]] }))
.toThrow(`Invalid RPC serialized data: type "${type}" is not supported.`)
expect(serializer.deserializeJson({ json: 1, meta: [[type]] })).toBe(1)
})

it('throws instead of producing garbage for corrupted built-in payloads', () => {
Expand Down
8 changes: 5 additions & 3 deletions packages/client/src/rpc-json-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ export interface RPCJsonSerializerOptions {
* handlers: { url: undefined }
* ```
*
* During deserialization, values whose type has no handler are left in their serialized form,
* so a client and server with different handlers can still communicate.
*
* Built-in type keys: `undefined`, `bigint`, `date`, `nan`, `infinity`, `url`, `set`, `map`.
*/
handlers?: Record<string, undefined | RPCJsonSerializerHandler> | undefined
Expand Down Expand Up @@ -454,11 +457,10 @@ export class RPCJsonSerializer {
let chainDepth = 0

for (const item of meta) {
const type = item[0]
const handler = this.handlers[type]
const handler = this.handlers[item[0]]

if (handler === undefined) {
throw invalidSerializedData(`type "${type}" is not supported.`)
continue
}

const depth = item.length - 1
Expand Down
5 changes: 0 additions & 5 deletions packages/client/src/rpc-serializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,10 +381,5 @@ describe('rpcSerializer', () => {
// only a Blob can land there, and the spec rejects it as an array length
expect(() => serializer.deserialize(form)).toThrow(RangeError)
})

it('rejects unknown meta types instead of resolving them on the prototype chain', () => {
expect(() => serializer.deserialize({ json: 1, meta: [['constructor']] })).toThrow()
expect(() => serializer.deserialize({ json: 1, meta: [['__proto__']] })).toThrow()
})
})
})
Loading