Skip to content

Commit bd87f7e

Browse files
jameskranzclaude
andcommitted
fix(server): guard traverseContractProcedures against null/undefined exports
getHiddenRouterContract reads a symbol property and throws on null or undefined. Recursing into a child export of `null` (e.g. `export const X = null`) crashed before any guard ran. Move the typeof/null guard above the hidden-contract lookup, and update the test to use a fixture containing null/undefined so the crash path is covered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 92b7eab commit bd87f7e

File tree

2 files changed

+11
-11
lines changed

2 files changed

+11
-11
lines changed

packages/server/src/router-utils.test.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -290,18 +290,11 @@ describe('router modules that export primitives alongside procedures', () => {
290290
})
291291

292292
describe('traverseContractProcedures', () => {
293-
it('traverses procedures and skips primitive exports', () => {
294-
// null/undefined are excluded here because getHiddenRouterContract
295-
// is called before the type guard and does not handle null
296-
const moduleWithStringExports = {
297-
getUser: pong,
298-
listUsers: pong,
299-
API_VERSION: 'v2',
300-
MAX_PAGE_SIZE: 100,
301-
ENABLE_CACHE: true,
302-
} as any
293+
it('traverses procedures and skips primitive, null, and undefined exports', () => {
303294
const callback = vi.fn()
304-
traverseContractProcedures({ router: moduleWithStringExports, path: [] }, callback)
295+
expect(() =>
296+
traverseContractProcedures({ router: moduleWithPrimitives, path: [] }, callback),
297+
).not.toThrow()
305298
expect(callback).toHaveBeenCalledTimes(2)
306299
expect(callback).toHaveBeenCalledWith({ contract: pong, path: ['getUser'] })
307300
expect(callback).toHaveBeenCalledWith({ contract: pong, path: ['listUsers'] })

packages/server/src/router-utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,13 @@ export function traverseContractProcedures(
192192
callback: (options: TraverseContractProcedureCallbackOptions) => void,
193193
lazyOptions: LazyTraverseContractProceduresOptions[] = [],
194194
): LazyTraverseContractProceduresOptions[] {
195+
// Guard before reading the hidden-contract symbol so that null/undefined
196+
// child exports don't crash in `getHiddenRouterContract`. Primitives like
197+
// strings autobox safely; only null/undefined throw on symbol access.
198+
if (typeof options.router !== 'object' || options.router === null) {
199+
return lazyOptions
200+
}
201+
195202
let currentRouter: AnyContractRouter | Lazyable<AnyRouter> = options.router
196203

197204
const hiddenContract = getHiddenRouterContract(options.router)

0 commit comments

Comments
 (0)