Environment
- @orpc/server: 1.14.1
- @orpc/openapi: 1.14.1
- @orpc/zod: 1.14.1
- TypeScript: 5.x
- Node.js: 24.x
- OS: Linux
Reproduction
import { OpenAPIGenerator, OpenAPIHandler } from '@orpc/openapi'
import { os } from '@orpc/server'
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
import { z } from 'zod'
const plainProcedure = os
.route({ method: 'GET', path: '/plain' })
.output(z.object({ ok: z.boolean() }))
.handler(async () => ({ ok: true }))
const callableProcedure = os
.route({ method: 'GET', path: '/callable' })
.output(z.object({ ok: z.boolean() }))
.handler(async () => ({ ok: true }))
.callable()
const actionableProcedure = os
.route({ method: 'GET', path: '/actionable' })
.output(z.object({ ok: z.boolean() }))
.handler(async () => ({ ok: true }))
.actionable()
const router = {
plain: plainProcedure,
callable: callableProcedure,
actionable: actionableProcedure,
}
const generator = new OpenAPIGenerator({
schemaConverters: [new ZodToJsonSchemaConverter()],
})
const spec = await generator.generate(router, {
info: {
title: 'Repro',
version: '1.0.0',
},
})
console.log(Object.keys(spec.paths))
// Actual on 1.14.1: ["plain"]
// Expected: ["plain", "callable", "actionable"]
const handler = new OpenAPIHandler(router)
const { matched } = await handler.handle(
new Request('http://localhost/callable'),
)
console.log(matched)
// Actual on 1.14.1: false
// Expected: true
Describe the bug
Procedures transformed with .callable() or .actionable() are skipped by OpenAPI router traversal in @orpc/server / @orpc/openapi 1.14.1.
The procedures still expose ~orpc and are recognized as procedure-like values, but their runtime type is function because .callable() and .actionable() return function proxies. After PR #1522, traverseContractProcedures returns early for non-object router values:
if (typeof options.router !== 'object' || options.router === null) {
return lazyOptions
}
Because this guard runs before isContractProcedure(...), function-valued procedures are treated like primitive exports and skipped. This causes:
OpenAPIGenerator to omit callable/actionable procedures from generated specs.
OpenAPIHandler to fail matching callable/actionable procedures and return matched: false.
This appears to be a regression from 1.13.x. In 1.13.5, traverseContractProcedures checked isContractProcedure(currentRouter) without first excluding function values, so callable/actionable procedure proxies were discovered correctly.
Additional context
PR #1522 fixed an issue where router modules exporting primitives could cause traversal problems or stack overflows. However, ORPC procedures can validly be function-valued after .callable() or .actionable().
A minimal fix may be to recognize procedure-like values before applying the primitive guard, for example by checking isContractProcedure(...) / isProcedure(...) before returning early for non-objects, or by allowing function values that expose a valid ~orpc procedure definition.
Environment
Reproduction
Describe the bug
Procedures transformed with
.callable()or.actionable()are skipped by OpenAPI router traversal in@orpc/server/@orpc/openapi1.14.1.The procedures still expose
~orpcand are recognized as procedure-like values, but their runtime type isfunctionbecause.callable()and.actionable()return function proxies. After PR #1522,traverseContractProceduresreturns early for non-object router values:Because this guard runs before
isContractProcedure(...), function-valued procedures are treated like primitive exports and skipped. This causes:OpenAPIGeneratorto omit callable/actionable procedures from generated specs.OpenAPIHandlerto fail matching callable/actionable procedures and returnmatched: false.This appears to be a regression from 1.13.x. In 1.13.5,
traverseContractProcedurescheckedisContractProcedure(currentRouter)without first excluding function values, so callable/actionable procedure proxies were discovered correctly.Additional context
PR #1522 fixed an issue where router modules exporting primitives could cause traversal problems or stack overflows. However, ORPC procedures can validly be function-valued after
.callable()or.actionable().A minimal fix may be to recognize procedure-like values before applying the primitive guard, for example by checking
isContractProcedure(...)/isProcedure(...)before returning early for non-objects, or by allowing function values that expose a valid~orpcprocedure definition.