fix(server,contract): guard against primitive values in router tree traversal#1522
fix(server,contract): guard against primitive values in router tree traversal#1522jameskranz wants to merge 3 commits intomiddleapi:mainfrom
Conversation
`enhanceRouter`, `traverseContractProcedures`, and `unlazyRouter` all use `for (const key in router)` without verifying the value is an object. When a router module re-exports a primitive (e.g. `export const FOO = 'bar'`), the `for...in` loop iterates string character indices, eventually hitting a single-character string that recurses on itself infinitely — crashing with RangeError: Maximum call stack size exceeded. Add type guards before the `for...in` fallback in all three functions so that non-object values are returned/skipped instead of iterated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use .toEqual() with expected structure instead of .toBeDefined() for the unlazyRouter primitive-value test case. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces safety guards in packages/server/src/router-utils.ts to prevent infinite recursion when traversing router trees that contain non-object primitive values. It also adds descriptive JSDoc comments to several utility functions and includes a new suite of tests to verify correct handling of various primitive types. Feedback suggests that similar logic in the contract package should also be updated to prevent potential crashes in those modules.
| if (typeof router !== 'object' || router === null) { | ||
| return router as any | ||
| } |
There was a problem hiding this comment.
The infinite recursion issue addressed here also affects several functions in the contract package. enhanceContractRouter, minifyContractRouter, and populateContractRouterPaths in packages/contract/src/router-utils.ts all utilize for...in loops on router objects without checking for primitives. To ensure full system stability and prevent similar crashes when processing contract routers that might contain non-router exports, those functions should be updated with similar type guards.
32952fd to
92ffa80
Compare
📝 WalkthroughWalkthroughAdded defensive guards to router utilities so primitive or null exports are returned unchanged and not recursed into; tests added in server and contract packages to validate primitives alongside procedure exports and to prevent infinite recursion/throws. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
92ffa80 to
24438df
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/contract/src/router-utils.test.ts (2)
111-129: Consider adding boolean and null/undefined tests for consistency.
enhanceContractRoutertests cover boolean and null/undefined values (lines 98-108), but this suite doesn't. While the guard implementation handles all primitive types uniformly, adding these tests would ensure consistent coverage across all three functions.💡 Suggested additional test cases
it('handles number values without infinite recursion', () => { const routerWithNumber = { ping, VERSION: 42 } as any const minified = minifyContractRouter(routerWithNumber) expect(isContractProcedure((minified as any).ping)).toBe(true) }) + + it('handles boolean values without infinite recursion', () => { + const routerWithBool = { ping, ENABLED: true } as any + const minified = minifyContractRouter(routerWithBool) + expect(isContractProcedure((minified as any).ping)).toBe(true) + }) + + it('handles null and undefined values without infinite recursion', () => { + const routerWithNullish = { ping, NIL: null, UNDEF: undefined } as any + const minified = minifyContractRouter(routerWithNullish) + expect(isContractProcedure((minified as any).ping)).toBe(true) + }) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/contract/src/router-utils.test.ts` around lines 111 - 129, Add tests to the "minifyContractRouter with primitive values" suite to cover boolean and null/undefined cases similar to the existing string/number tests: create router objects like { ping, FLAG: true } and { ping, MISSING: null } (or undefined), call minifyContractRouter on each, and assert isContractProcedure((minified as any).ping) is true; place the tests as additional it(...) blocks alongside the existing ones to ensure parity with enhanceContractRouter coverage.
131-149: Same suggestion: consider adding boolean and null/undefined tests.For the same consistency reason as
minifyContractRouter, adding tests for boolean and null/undefined would complete the coverage parity withenhanceContractRouter.💡 Suggested additional test cases
it('handles number values without infinite recursion', () => { const routerWithNumber = { ping: oc.input(inputSchema), VERSION: 42 } as any const populated = populateContractRouterPaths(routerWithNumber) expect(isContractProcedure(populated.ping)).toBe(true) }) + + it('handles boolean values without infinite recursion', () => { + const routerWithBool = { ping: oc.input(inputSchema), ENABLED: true } as any + const populated = populateContractRouterPaths(routerWithBool) + expect(isContractProcedure(populated.ping)).toBe(true) + }) + + it('handles null and undefined values without infinite recursion', () => { + const routerWithNullish = { ping: oc.input(inputSchema), NIL: null, UNDEF: undefined } as any + const populated = populateContractRouterPaths(routerWithNullish) + expect(isContractProcedure(populated.ping)).toBe(true) + }) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/contract/src/router-utils.test.ts` around lines 131 - 149, Add tests to populateContractRouterPaths to cover boolean and null/undefined primitive values similar to the existing string/number tests: create router variations like { ping: oc.input(inputSchema), FLAG: true } and { ping: oc.input(inputSchema), MISSING: null } (and undefined), call populateContractRouterPaths on each, and assert expect(isContractProcedure(populated.ping)).toBe(true). Mirror the style and naming of the current cases so coverage matches minifyContractRouter and enhanceContractRouter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/contract/src/router-utils.test.ts`:
- Around line 111-129: Add tests to the "minifyContractRouter with primitive
values" suite to cover boolean and null/undefined cases similar to the existing
string/number tests: create router objects like { ping, FLAG: true } and { ping,
MISSING: null } (or undefined), call minifyContractRouter on each, and assert
isContractProcedure((minified as any).ping) is true; place the tests as
additional it(...) blocks alongside the existing ones to ensure parity with
enhanceContractRouter coverage.
- Around line 131-149: Add tests to populateContractRouterPaths to cover boolean
and null/undefined primitive values similar to the existing string/number tests:
create router variations like { ping: oc.input(inputSchema), FLAG: true } and {
ping: oc.input(inputSchema), MISSING: null } (and undefined), call
populateContractRouterPaths on each, and assert
expect(isContractProcedure(populated.ping)).toBe(true). Mirror the style and
naming of the current cases so coverage matches minifyContractRouter and
enhanceContractRouter.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b4523cde-6f58-4035-85e1-37ca5c843057
📒 Files selected for processing (2)
packages/contract/src/router-utils.test.tspackages/contract/src/router-utils.ts
Apply the same primitive type guards to enhanceContractRouter, minifyContractRouter, and populateContractRouterPaths in the contract package. These functions have the same infinite recursion vulnerability when router modules export string values alongside contract procedures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9167d2b to
f2fc836
Compare
|
But why is this necessary? Why doesn't TypeScript warn you that the oRPC router is invalid when it contains non-procedure properties? |
The Router mapped type does map non-procedure properties to never, so in theory TypeScript should catch this. But it doesn't when you use import * as module to compose routers -- the namespace type is wide enough to slip past the constraint, and a few internal .router() paths cast through as any. We've hit this twice in our codebase. Both times we were using import * as namespace imports, which is pretty much the standard way to compose routers from module files. Once it was a string constant exported alongside procedures, and once it was an exported object with circular internals. No TypeScript errors either time. Since the type system has this gap with namespace imports, it seems like the runtime should just skip non-procedure values instead of blowing up the stack. The change itself is pretty small -- just a typeof check before the for...in fallback, same pattern traverseContractProcedures already uses. |
|
@jameskranz This PR doesn't actually fix the circular object case mentioned. The The A cleaner approach would be filtering non-procedure exports at the boundary before they enter the router at all: function filterInternalExport<T extends Record<any, any>>(module: T): T {
return Object.fromEntries(
Object.entries(module).filter(([, v]) => return false for internal exports)
)
}
// Usage
import * as userModule from './user'
const router = { user: filterInternalExport(userModule) } |
Summary
for (const key in router)loops without validating that values are objects crash withRangeError: Maximum call stack size exceededwhen a router module exports primitives (e.g.,export const FOO = "bar").enhanceRouter,traverseContractProcedures, andunlazyRouterinpackages/server/src/router-utils.ts.enhanceContractRouter,minifyContractRouter, andpopulateContractRouterPathsinpackages/contract/src/router-utils.ts.Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests