diff --git a/apps/content/docs/rpc/serializer.mdx b/apps/content/docs/rpc/serializer.mdx index a18513f75..ae4567fce 100644 --- a/apps/content/docs/rpc/serializer.mdx +++ b/apps/content/docs/rpc/serializer.mdx @@ -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: diff --git a/packages/client/src/rpc-json-serializer.test.ts b/packages/client/src/rpc-json-serializer.test.ts index 4a733f1d5..18b5823d5 100644 --- a/packages/client/src/rpc-json-serializer.test.ts +++ b/packages/client/src/rpc-json-serializer.test.ts @@ -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() @@ -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', () => { diff --git a/packages/client/src/rpc-json-serializer.ts b/packages/client/src/rpc-json-serializer.ts index 9243d1a5f..5c092b10a 100644 --- a/packages/client/src/rpc-json-serializer.ts +++ b/packages/client/src/rpc-json-serializer.ts @@ -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 | undefined @@ -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 diff --git a/packages/client/src/rpc-serializer.test.ts b/packages/client/src/rpc-serializer.test.ts index 974ef9036..fbae71df2 100644 --- a/packages/client/src/rpc-serializer.test.ts +++ b/packages/client/src/rpc-serializer.test.ts @@ -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() - }) }) })