From de22b4dcfdda3d6b5f2b642a7b5aa7458abefc16 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 09:42:06 +0000 Subject: [PATCH 1/7] fix(tanstack-query,swr,pinia-colada): allow router utils to be converted to primitive values Router utils proxies wrap a null-prototype object from bindMethods, so valueOf/toString resolved to undefined and String(utils), template literals and React 19.2 dev props-diff logging threw "Cannot convert object to primitive value". Unwrap keys now fall back to Object.prototype; then/toJSON stay undefined. Similar to the tRPC fix in trpc/trpc#7336. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UDG8EYcjqwAUxqRdkQiEbA --- packages/pinia-colada/src/router-utils.test.ts | 11 ++++++++++- packages/pinia-colada/src/router-utils.ts | 8 +++++++- packages/swr/src/router-utils.test.ts | 10 ++++++++++ packages/swr/src/router-utils.ts | 8 +++++++- packages/tanstack-query/src/router-utils.test.ts | 10 ++++++++++ packages/tanstack-query/src/router-utils.ts | 8 +++++++- 6 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/pinia-colada/src/router-utils.test.ts b/packages/pinia-colada/src/router-utils.test.ts index 477a585336..c5684951f1 100644 --- a/packages/pinia-colada/src/router-utils.test.ts +++ b/packages/pinia-colada/src/router-utils.test.ts @@ -1,5 +1,5 @@ import type { RouterUtilsPlugin } from './plugin' -import { RPCJsonSerializer } from '@orpc/client' +import { createORPCClient, RPCJsonSerializer } from '@orpc/client' import * as KeyModule from './key' import { ProcedureUtils } from './procedure-utils' import { createRouterUtils } from './router-utils' @@ -100,6 +100,15 @@ describe('createRouterUtils', () => { expect(utils[Symbol.for('a')]).toBe(undefined) }) + it('can be converted to primitive values', () => { + const utils = createRouterUtils(createORPCClient({ call: vi.fn() }) as any) as any + + expect(String(utils.planet)).toBe('[object Object]') + expect(`${utils.planet.find.queryOptions.name}`).toBe('[object Object]') + expect(utils.planet.then).toBeUndefined() + expect(utils.planet.toJSON).toBeUndefined() + }) + it('stops recursive on RECURSIVE_CLIENT_UNWRAP_KEYS', () => { client.then = vi.fn() diff --git a/packages/pinia-colada/src/router-utils.ts b/packages/pinia-colada/src/router-utils.ts index 830847a75d..cb705c69e5 100644 --- a/packages/pinia-colada/src/router-utils.ts +++ b/packages/pinia-colada/src/router-utils.ts @@ -125,9 +125,15 @@ function createRouterUtilsInternal( const recursive = new Proxy(utils, { get(target, prop) { const value = Reflect.get(target, prop) + + if (typeof prop === 'string' && RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { + // utils have a null prototype, so fall back to Object.prototype to keep primitive conversion (valueOf/toString) working + return value ?? Reflect.get(Object.prototype, prop) + } + const nextClient = Reflect.get(client, prop) - if (typeof prop !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop) || !isTypescriptObject(nextClient)) { + if (typeof prop !== 'string' || !isTypescriptObject(nextClient)) { return value } diff --git a/packages/swr/src/router-utils.test.ts b/packages/swr/src/router-utils.test.ts index 564824fdc9..ef53568276 100644 --- a/packages/swr/src/router-utils.test.ts +++ b/packages/swr/src/router-utils.test.ts @@ -1,3 +1,4 @@ +import { createORPCClient } from '@orpc/client' import { ProcedureUtils } from './procedure-utils' import { createRouterUtils } from './router-utils' @@ -91,6 +92,15 @@ describe('createRouterUtils', () => { expect(utils[Symbol.for('a')]).toBe(undefined) }) + it('can be converted to primitive values', () => { + const utils = createRouterUtils(createORPCClient({ call: vi.fn() }) as any) as any + + expect(String(utils.planet)).toBe('[object Object]') + expect(`${utils.planet.find.fetcher.name}`).toBe('[object Object]') + expect(utils.planet.then).toBeUndefined() + expect(utils.planet.toJSON).toBeUndefined() + }) + it('stops recursive on symbol and RECURSIVE_CLIENT_UNWRAP_KEYS of conflicting method proxies', () => { const utils = createRouterUtils(client) as any const keyUtils = utils.key // "key" is both a util method and a nested client diff --git a/packages/swr/src/router-utils.ts b/packages/swr/src/router-utils.ts index d94ecf701f..b041bdad60 100644 --- a/packages/swr/src/router-utils.ts +++ b/packages/swr/src/router-utils.ts @@ -46,9 +46,15 @@ export function createRouterUtils( const recursive = new Proxy(utils, { get(target, prop) { const value = Reflect.get(target, prop) + + if (typeof prop === 'string' && RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { + // utils have a null prototype, so fall back to Object.prototype to keep primitive conversion (valueOf/toString) working + return value ?? Reflect.get(Object.prototype, prop) + } + const nextClient = Reflect.get(client, prop) - if (typeof prop !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop) || !isTypescriptObject(nextClient)) { + if (typeof prop !== 'string' || !isTypescriptObject(nextClient)) { return value } diff --git a/packages/tanstack-query/src/router-utils.test.ts b/packages/tanstack-query/src/router-utils.test.ts index 815249c602..f890a382f4 100644 --- a/packages/tanstack-query/src/router-utils.test.ts +++ b/packages/tanstack-query/src/router-utils.test.ts @@ -1,4 +1,5 @@ import type { RouterUtilsPlugin } from './plugin' +import { createORPCClient } from '@orpc/client' import * as KeyModule from './key' import { ProcedureUtils } from './procedure-utils' import { createRouterUtils } from './router-utils' @@ -91,6 +92,15 @@ describe('createRouterUtils', () => { expect(utils[Symbol.for('a')]).toBe(undefined) }) + it('can be converted to primitive values', () => { + const utils = createRouterUtils(createORPCClient({ call: vi.fn() }) as any) as any + + expect(String(utils.planet)).toBe('[object Object]') + expect(`${utils.planet.find.queryOptions.name}`).toBe('[object Object]') + expect(utils.planet.then).toBeUndefined() + expect(utils.planet.toJSON).toBeUndefined() + }) + it('supports scoped options', () => { const keyOptions = { queryOptions: { diff --git a/packages/tanstack-query/src/router-utils.ts b/packages/tanstack-query/src/router-utils.ts index 9d789e77c7..95e9d8889b 100644 --- a/packages/tanstack-query/src/router-utils.ts +++ b/packages/tanstack-query/src/router-utils.ts @@ -124,9 +124,15 @@ function createRouterUtilsInternal( const recursive = new Proxy(utils, { get(target, prop) { const value = Reflect.get(target, prop) + + if (typeof prop === 'string' && RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { + // utils have a null prototype, so fall back to Object.prototype to keep primitive conversion (valueOf/toString) working + return value ?? Reflect.get(Object.prototype, prop) + } + const nextClient = Reflect.get(client, prop) - if (typeof prop !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop) || !isTypescriptObject(nextClient)) { + if (typeof prop !== 'string' || !isTypescriptObject(nextClient)) { return value } From b045a105b1b69e1b7d50d173ba6747609e606d67 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 06:12:49 +0000 Subject: [PATCH 2/7] test(tanstack-query,swr,pinia-colada): use existing client fixture in primitive conversion tests The regression tests built a separate createORPCClient just to reach nested utils; the existing fixture already exercises the same proxy path (root utils and utils reached through a conflicting method proxy). Drop `then` assertions where the RECURSIVE_CLIENT_UNWRAP_KEYS test already covers them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015iL1oczPjpwbYpfCYwSFdo --- packages/pinia-colada/src/router-utils.test.ts | 11 +++++------ packages/swr/src/router-utils.test.ts | 10 ++++------ packages/tanstack-query/src/router-utils.test.ts | 12 ++++++------ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/pinia-colada/src/router-utils.test.ts b/packages/pinia-colada/src/router-utils.test.ts index c5684951f1..33009b77d9 100644 --- a/packages/pinia-colada/src/router-utils.test.ts +++ b/packages/pinia-colada/src/router-utils.test.ts @@ -1,5 +1,5 @@ import type { RouterUtilsPlugin } from './plugin' -import { createORPCClient, RPCJsonSerializer } from '@orpc/client' +import { RPCJsonSerializer } from '@orpc/client' import * as KeyModule from './key' import { ProcedureUtils } from './procedure-utils' import { createRouterUtils } from './router-utils' @@ -101,12 +101,11 @@ describe('createRouterUtils', () => { }) it('can be converted to primitive values', () => { - const utils = createRouterUtils(createORPCClient({ call: vi.fn() }) as any) as any + const utils = createRouterUtils(client) as any - expect(String(utils.planet)).toBe('[object Object]') - expect(`${utils.planet.find.queryOptions.name}`).toBe('[object Object]') - expect(utils.planet.then).toBeUndefined() - expect(utils.planet.toJSON).toBeUndefined() + expect(String(utils)).toBe('[object Object]') + expect(`${utils.key.pong}`).toBe('[object Object]') + expect(utils.toJSON).toBe(undefined) }) it('stops recursive on RECURSIVE_CLIENT_UNWRAP_KEYS', () => { diff --git a/packages/swr/src/router-utils.test.ts b/packages/swr/src/router-utils.test.ts index ef53568276..85edcd4a1f 100644 --- a/packages/swr/src/router-utils.test.ts +++ b/packages/swr/src/router-utils.test.ts @@ -1,4 +1,3 @@ -import { createORPCClient } from '@orpc/client' import { ProcedureUtils } from './procedure-utils' import { createRouterUtils } from './router-utils' @@ -93,12 +92,11 @@ describe('createRouterUtils', () => { }) it('can be converted to primitive values', () => { - const utils = createRouterUtils(createORPCClient({ call: vi.fn() }) as any) as any + const utils = createRouterUtils(client) as any - expect(String(utils.planet)).toBe('[object Object]') - expect(`${utils.planet.find.fetcher.name}`).toBe('[object Object]') - expect(utils.planet.then).toBeUndefined() - expect(utils.planet.toJSON).toBeUndefined() + expect(String(utils)).toBe('[object Object]') + expect(`${utils.key.pong}`).toBe('[object Object]') + expect(utils.toJSON).toBe(undefined) }) it('stops recursive on symbol and RECURSIVE_CLIENT_UNWRAP_KEYS of conflicting method proxies', () => { diff --git a/packages/tanstack-query/src/router-utils.test.ts b/packages/tanstack-query/src/router-utils.test.ts index b22d856d7b..a85ba6c578 100644 --- a/packages/tanstack-query/src/router-utils.test.ts +++ b/packages/tanstack-query/src/router-utils.test.ts @@ -1,5 +1,5 @@ import type { RouterUtilsPlugin } from './plugin' -import { createORPCClient, RPCJsonSerializer } from '@orpc/client' +import { RPCJsonSerializer } from '@orpc/client' import * as KeyModule from './key' import { ProcedureUtils } from './procedure-utils' import { createRouterUtils } from './router-utils' @@ -95,12 +95,12 @@ describe('createRouterUtils', () => { }) it('can be converted to primitive values', () => { - const utils = createRouterUtils(createORPCClient({ call: vi.fn() }) as any) as any + const utils = createRouterUtils(client) as any - expect(String(utils.planet)).toBe('[object Object]') - expect(`${utils.planet.find.queryOptions.name}`).toBe('[object Object]') - expect(utils.planet.then).toBeUndefined() - expect(utils.planet.toJSON).toBeUndefined() + expect(String(utils)).toBe('[object Object]') + expect(`${utils.key.pong}`).toBe('[object Object]') + expect(utils.then).toBe(undefined) + expect(utils.toJSON).toBe(undefined) }) it('supports scoped options', () => { From c51354c96b261fd109495b248aa8a48b76965001 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 06:41:15 +0000 Subject: [PATCH 3/7] refactor(client): type RECURSIVE_CLIENT_UNWRAP_KEYS as Set Lets proxy traps check `prop` directly without first narrowing it to a string, so the router-utils unwrap check drops its `typeof` guard. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015iL1oczPjpwbYpfCYwSFdo --- packages/client/src/consts.ts | 2 +- packages/pinia-colada/src/router-utils.ts | 2 +- packages/swr/src/router-utils.ts | 2 +- packages/tanstack-query/src/router-utils.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/client/src/consts.ts b/packages/client/src/consts.ts index 144e881997..4dc4665b24 100644 --- a/packages/client/src/consts.ts +++ b/packages/client/src/consts.ts @@ -7,7 +7,7 @@ * proxy for them can cause unexpected behavior, compatibility issues, or * infinite proxy chains. */ -export const RECURSIVE_CLIENT_UNWRAP_KEYS = new Set([ +export const RECURSIVE_CLIENT_UNWRAP_KEYS = new Set([ /** * Prevents the client from being treated as a thenable when users * accidentally write `await client`. diff --git a/packages/pinia-colada/src/router-utils.ts b/packages/pinia-colada/src/router-utils.ts index cb705c69e5..73b63a5fa6 100644 --- a/packages/pinia-colada/src/router-utils.ts +++ b/packages/pinia-colada/src/router-utils.ts @@ -126,7 +126,7 @@ function createRouterUtilsInternal( get(target, prop) { const value = Reflect.get(target, prop) - if (typeof prop === 'string' && RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { + if (RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { // utils have a null prototype, so fall back to Object.prototype to keep primitive conversion (valueOf/toString) working return value ?? Reflect.get(Object.prototype, prop) } diff --git a/packages/swr/src/router-utils.ts b/packages/swr/src/router-utils.ts index b041bdad60..f665f94c5b 100644 --- a/packages/swr/src/router-utils.ts +++ b/packages/swr/src/router-utils.ts @@ -47,7 +47,7 @@ export function createRouterUtils( get(target, prop) { const value = Reflect.get(target, prop) - if (typeof prop === 'string' && RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { + if (RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { // utils have a null prototype, so fall back to Object.prototype to keep primitive conversion (valueOf/toString) working return value ?? Reflect.get(Object.prototype, prop) } diff --git a/packages/tanstack-query/src/router-utils.ts b/packages/tanstack-query/src/router-utils.ts index 8bf1386e7a..b89fde0855 100644 --- a/packages/tanstack-query/src/router-utils.ts +++ b/packages/tanstack-query/src/router-utils.ts @@ -126,7 +126,7 @@ function createRouterUtilsInternal( get(target, prop) { const value = Reflect.get(target, prop) - if (typeof prop === 'string' && RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { + if (RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { // utils have a null prototype, so fall back to Object.prototype to keep primitive conversion (valueOf/toString) working return value ?? Reflect.get(Object.prototype, prop) } From 6765f26153f864f0f2452f6300ab7d8d00fd4463 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 06:45:10 +0000 Subject: [PATCH 4/7] fix(contract): allow error constructor map to be converted to primitive values The error constructor map returned a constructor for every key except `then`, so `toString`/`valueOf` returned ORPCError objects and `String(errors)` threw "Cannot convert object to primitive value", while `JSON.stringify(errors)` produced a fake error with code "toJSON". It now resolves all RECURSIVE_CLIENT_UNWRAP_KEYS to the error map value, like the client proxies. Docs list these names as reserved error codes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015iL1oczPjpwbYpfCYwSFdo --- apps/content/docs/error-handling.mdx | 4 ++++ packages/contract/src/error-factory.test.ts | 11 +++++++++-- packages/contract/src/error-factory.ts | 7 +++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/content/docs/error-handling.mdx b/apps/content/docs/error-handling.mdx index 4c8b549a35..2f9e247dd9 100644 --- a/apps/content/docs/error-handling.mdx +++ b/apps/content/docs/error-handling.mdx @@ -79,6 +79,10 @@ const exampleProcedure = os }) ``` +:::warning +For compatibility, do not use these error codes: `then`, `bind`, `call`, `apply`, `valueOf`, `toString`, `toJSON`. +::: + :::tip You can use typesafe errors across your entire project, but we recommend reserving them for application-specific cases. For common errors like `UNAUTHORIZED` or `RATE_LIMITED`, the client usually already understands the meaning. Skipping explicit schemas for those errors can also reduce type complexity and improve TypeScript performance. ::: diff --git a/packages/contract/src/error-factory.test.ts b/packages/contract/src/error-factory.test.ts index e951dac69b..aacaafd4e3 100644 --- a/packages/contract/src/error-factory.test.ts +++ b/packages/contract/src/error-factory.test.ts @@ -230,10 +230,17 @@ describe('createORPCErrorConstructorMap', () => { expect(await map).toBe(map) }) + it('can be converted to primitive values', () => { + const map = constructors as any + + expect(String(map)).toBe('[object Object]') + expect(map.toJSON).toBe(undefined) + }) + it('does not resolve error codes through Object.prototype', () => { - const e = (constructors as any).toString() + const e = (constructors as any).constructor() - expect(e.code).toEqual('toString') + expect(e.code).toEqual('constructor') expect(e.defined).toEqual(false) }) }) diff --git a/packages/contract/src/error-factory.ts b/packages/contract/src/error-factory.ts index b0001c8727..ce2db1b7f1 100644 --- a/packages/contract/src/error-factory.ts +++ b/packages/contract/src/error-factory.ts @@ -3,7 +3,7 @@ import type { MaybeOptionalOptions, Writable } from '@orpc/shared' import type { ErrorMap, ErrorMapItem } from './error' import type { AnySchema, InferSchemaInput, Schema } from './schema' -import { ORPCError } from '@orpc/client' +import { ORPCError, RECURSIVE_CLIENT_UNWRAP_KEYS } from '@orpc/client' import { getOwn, resolveMaybeOptionalOptions } from '@orpc/shared' import { ValidationError } from './error' import { type } from './schema-utils' @@ -138,6 +138,9 @@ export type ORPCErrorConstructorMap = { * - If the code exists in the provided `errorMap`, the corresponding constructor * will create a **defined** `ORPCError`. * - If the code does not exist, a fallback `ORPCError` constructor is returned. + * - Symbols and {@link RECURSIVE_CLIENT_UNWRAP_KEYS} (`then`, `toString`, ...) + * resolve to the `errorMap` value instead, so the map is not thenable and can + * be converted to a string. * * The `in` operator can be used to check whether an error code is explicitly * defined in the map. @@ -156,7 +159,7 @@ export type ORPCErrorConstructorMap = { export function createORPCErrorConstructorMap(errorMap: T): ORPCErrorConstructorMap { const proxy = new Proxy(errorMap, { get(target, code) { - if (typeof code !== 'string' || code === 'then') { + if (typeof code !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(code)) { return Reflect.get(target, code) } From 1b7f5a55ea70da41245178cb283f80fad14fef52 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 06:53:25 +0000 Subject: [PATCH 5/7] refactor: trim primitive conversion tests and drop comments covered by tests Router-utils tests keep only `String(utils)`; the other assertions passed without the fix or exercised the same trap. Fold the error map check into the existing unwrap-keys test. Remove the inline and JSDoc comments that restate what these tests cover. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015iL1oczPjpwbYpfCYwSFdo --- packages/contract/src/error-factory.test.ts | 9 ++------- packages/contract/src/error-factory.ts | 3 --- packages/pinia-colada/src/router-utils.test.ts | 3 --- packages/pinia-colada/src/router-utils.ts | 1 - packages/swr/src/router-utils.test.ts | 3 --- packages/swr/src/router-utils.ts | 1 - packages/tanstack-query/src/router-utils.test.ts | 4 ---- packages/tanstack-query/src/router-utils.ts | 1 - 8 files changed, 2 insertions(+), 23 deletions(-) diff --git a/packages/contract/src/error-factory.test.ts b/packages/contract/src/error-factory.test.ts index aacaafd4e3..eef5149bd6 100644 --- a/packages/contract/src/error-factory.test.ts +++ b/packages/contract/src/error-factory.test.ts @@ -223,18 +223,13 @@ describe('createORPCErrorConstructorMap', () => { expect('ANY_THING' in constructors).toBe(false) }) - it('not proxy on symbol and then keys', async () => { + it('not proxy on symbol and RECURSIVE_CLIENT_UNWRAP_KEYS', async () => { const map = constructors as any expect(map[Symbol('something')]).toBeUndefined() expect(map.then).toBeUndefined() expect(await map).toBe(map) - }) - - it('can be converted to primitive values', () => { - const map = constructors as any - + expect(map.toJSON).toBeUndefined() expect(String(map)).toBe('[object Object]') - expect(map.toJSON).toBe(undefined) }) it('does not resolve error codes through Object.prototype', () => { diff --git a/packages/contract/src/error-factory.ts b/packages/contract/src/error-factory.ts index ce2db1b7f1..09c962f176 100644 --- a/packages/contract/src/error-factory.ts +++ b/packages/contract/src/error-factory.ts @@ -138,9 +138,6 @@ export type ORPCErrorConstructorMap = { * - If the code exists in the provided `errorMap`, the corresponding constructor * will create a **defined** `ORPCError`. * - If the code does not exist, a fallback `ORPCError` constructor is returned. - * - Symbols and {@link RECURSIVE_CLIENT_UNWRAP_KEYS} (`then`, `toString`, ...) - * resolve to the `errorMap` value instead, so the map is not thenable and can - * be converted to a string. * * The `in` operator can be used to check whether an error code is explicitly * defined in the map. diff --git a/packages/pinia-colada/src/router-utils.test.ts b/packages/pinia-colada/src/router-utils.test.ts index 33009b77d9..94b2487bac 100644 --- a/packages/pinia-colada/src/router-utils.test.ts +++ b/packages/pinia-colada/src/router-utils.test.ts @@ -102,10 +102,7 @@ describe('createRouterUtils', () => { it('can be converted to primitive values', () => { const utils = createRouterUtils(client) as any - expect(String(utils)).toBe('[object Object]') - expect(`${utils.key.pong}`).toBe('[object Object]') - expect(utils.toJSON).toBe(undefined) }) it('stops recursive on RECURSIVE_CLIENT_UNWRAP_KEYS', () => { diff --git a/packages/pinia-colada/src/router-utils.ts b/packages/pinia-colada/src/router-utils.ts index 73b63a5fa6..bb77a846a7 100644 --- a/packages/pinia-colada/src/router-utils.ts +++ b/packages/pinia-colada/src/router-utils.ts @@ -127,7 +127,6 @@ function createRouterUtilsInternal( const value = Reflect.get(target, prop) if (RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { - // utils have a null prototype, so fall back to Object.prototype to keep primitive conversion (valueOf/toString) working return value ?? Reflect.get(Object.prototype, prop) } diff --git a/packages/swr/src/router-utils.test.ts b/packages/swr/src/router-utils.test.ts index 85edcd4a1f..3f440d660a 100644 --- a/packages/swr/src/router-utils.test.ts +++ b/packages/swr/src/router-utils.test.ts @@ -93,10 +93,7 @@ describe('createRouterUtils', () => { it('can be converted to primitive values', () => { const utils = createRouterUtils(client) as any - expect(String(utils)).toBe('[object Object]') - expect(`${utils.key.pong}`).toBe('[object Object]') - expect(utils.toJSON).toBe(undefined) }) it('stops recursive on symbol and RECURSIVE_CLIENT_UNWRAP_KEYS of conflicting method proxies', () => { diff --git a/packages/swr/src/router-utils.ts b/packages/swr/src/router-utils.ts index f665f94c5b..83d4e1bb3f 100644 --- a/packages/swr/src/router-utils.ts +++ b/packages/swr/src/router-utils.ts @@ -48,7 +48,6 @@ export function createRouterUtils( const value = Reflect.get(target, prop) if (RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { - // utils have a null prototype, so fall back to Object.prototype to keep primitive conversion (valueOf/toString) working return value ?? Reflect.get(Object.prototype, prop) } diff --git a/packages/tanstack-query/src/router-utils.test.ts b/packages/tanstack-query/src/router-utils.test.ts index a85ba6c578..38260cd1da 100644 --- a/packages/tanstack-query/src/router-utils.test.ts +++ b/packages/tanstack-query/src/router-utils.test.ts @@ -96,11 +96,7 @@ describe('createRouterUtils', () => { it('can be converted to primitive values', () => { const utils = createRouterUtils(client) as any - expect(String(utils)).toBe('[object Object]') - expect(`${utils.key.pong}`).toBe('[object Object]') - expect(utils.then).toBe(undefined) - expect(utils.toJSON).toBe(undefined) }) it('supports scoped options', () => { diff --git a/packages/tanstack-query/src/router-utils.ts b/packages/tanstack-query/src/router-utils.ts index b89fde0855..fbeecf169e 100644 --- a/packages/tanstack-query/src/router-utils.ts +++ b/packages/tanstack-query/src/router-utils.ts @@ -127,7 +127,6 @@ function createRouterUtilsInternal( const value = Reflect.get(target, prop) if (RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { - // utils have a null prototype, so fall back to Object.prototype to keep primitive conversion (valueOf/toString) working return value ?? Reflect.get(Object.prototype, prop) } From e149af788f6fc774b411de83a6f92871451229a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 07:02:02 +0000 Subject: [PATCH 6/7] fix(contract): keep defined error codes constructible for unwrap keys Only undefined RECURSIVE_CLIENT_UNWRAP_KEYS codes resolve to the error map value, so codes such as `toJSON` or `call` declared in the error map still get constructors, matching ORPCErrorConstructorMap's types. `then` stays reserved as before. Drop the docs warning that reserved all seven names as error codes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015iL1oczPjpwbYpfCYwSFdo --- apps/content/docs/error-handling.mdx | 4 ---- packages/contract/src/error-factory.test.ts | 9 +++++++++ packages/contract/src/error-factory.ts | 6 +++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/content/docs/error-handling.mdx b/apps/content/docs/error-handling.mdx index 2f9e247dd9..4c8b549a35 100644 --- a/apps/content/docs/error-handling.mdx +++ b/apps/content/docs/error-handling.mdx @@ -79,10 +79,6 @@ const exampleProcedure = os }) ``` -:::warning -For compatibility, do not use these error codes: `then`, `bind`, `call`, `apply`, `valueOf`, `toString`, `toJSON`. -::: - :::tip You can use typesafe errors across your entire project, but we recommend reserving them for application-specific cases. For common errors like `UNAUTHORIZED` or `RATE_LIMITED`, the client usually already understands the meaning. Skipping explicit schemas for those errors can also reduce type complexity and improve TypeScript performance. ::: diff --git a/packages/contract/src/error-factory.test.ts b/packages/contract/src/error-factory.test.ts index eef5149bd6..3c33c56292 100644 --- a/packages/contract/src/error-factory.test.ts +++ b/packages/contract/src/error-factory.test.ts @@ -232,6 +232,15 @@ describe('createORPCErrorConstructorMap', () => { expect(String(map)).toBe('[object Object]') }) + it('still constructs defined RECURSIVE_CLIENT_UNWRAP_KEYS codes except then', async () => { + const map = createORPCErrorConstructorMap({ toJSON: {}, then: {} }) as any + + const e = map.toJSON() + expect(e.code).toEqual('toJSON') + expect(e.defined).toEqual(true) + expect(await map).toBe(map) + }) + it('does not resolve error codes through Object.prototype', () => { const e = (constructors as any).constructor() diff --git a/packages/contract/src/error-factory.ts b/packages/contract/src/error-factory.ts index 09c962f176..b334eb3239 100644 --- a/packages/contract/src/error-factory.ts +++ b/packages/contract/src/error-factory.ts @@ -156,7 +156,11 @@ export type ORPCErrorConstructorMap = { export function createORPCErrorConstructorMap(errorMap: T): ORPCErrorConstructorMap { const proxy = new Proxy(errorMap, { get(target, code) { - if (typeof code !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(code)) { + if ( + typeof code !== 'string' + || code === 'then' + || (RECURSIVE_CLIENT_UNWRAP_KEYS.has(code) && !Object.hasOwn(target, code)) + ) { return Reflect.get(target, code) } From 3d8beee80006b9cf9615034b3164d45d467961c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 07:06:44 +0000 Subject: [PATCH 7/7] Revert "fix(contract): keep defined error codes constructible for unwrap keys" This reverts commit e149af7. The error constructor map skips every RECURSIVE_CLIENT_UNWRAP_KEYS name again, and the docs list them as reserved error codes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015iL1oczPjpwbYpfCYwSFdo --- apps/content/docs/error-handling.mdx | 4 ++++ packages/contract/src/error-factory.test.ts | 9 --------- packages/contract/src/error-factory.ts | 6 +----- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/apps/content/docs/error-handling.mdx b/apps/content/docs/error-handling.mdx index 4c8b549a35..2f9e247dd9 100644 --- a/apps/content/docs/error-handling.mdx +++ b/apps/content/docs/error-handling.mdx @@ -79,6 +79,10 @@ const exampleProcedure = os }) ``` +:::warning +For compatibility, do not use these error codes: `then`, `bind`, `call`, `apply`, `valueOf`, `toString`, `toJSON`. +::: + :::tip You can use typesafe errors across your entire project, but we recommend reserving them for application-specific cases. For common errors like `UNAUTHORIZED` or `RATE_LIMITED`, the client usually already understands the meaning. Skipping explicit schemas for those errors can also reduce type complexity and improve TypeScript performance. ::: diff --git a/packages/contract/src/error-factory.test.ts b/packages/contract/src/error-factory.test.ts index 3c33c56292..eef5149bd6 100644 --- a/packages/contract/src/error-factory.test.ts +++ b/packages/contract/src/error-factory.test.ts @@ -232,15 +232,6 @@ describe('createORPCErrorConstructorMap', () => { expect(String(map)).toBe('[object Object]') }) - it('still constructs defined RECURSIVE_CLIENT_UNWRAP_KEYS codes except then', async () => { - const map = createORPCErrorConstructorMap({ toJSON: {}, then: {} }) as any - - const e = map.toJSON() - expect(e.code).toEqual('toJSON') - expect(e.defined).toEqual(true) - expect(await map).toBe(map) - }) - it('does not resolve error codes through Object.prototype', () => { const e = (constructors as any).constructor() diff --git a/packages/contract/src/error-factory.ts b/packages/contract/src/error-factory.ts index b334eb3239..09c962f176 100644 --- a/packages/contract/src/error-factory.ts +++ b/packages/contract/src/error-factory.ts @@ -156,11 +156,7 @@ export type ORPCErrorConstructorMap = { export function createORPCErrorConstructorMap(errorMap: T): ORPCErrorConstructorMap { const proxy = new Proxy(errorMap, { get(target, code) { - if ( - typeof code !== 'string' - || code === 'then' - || (RECURSIVE_CLIENT_UNWRAP_KEYS.has(code) && !Object.hasOwn(target, code)) - ) { + if (typeof code !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(code)) { return Reflect.get(target, code) }