fix(server): apply the contract to routers implemented with .lazy() - #2070
Conversation
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/experimental-lock
@orpc/experimental-msw
@orpc/nest
@orpc/next
@orpc/node
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Routers loaded through implement(contract).lazy(loader) now carry the hidden contract, so their procedures follow the contract's error map and meta exactly like .router(...) does.
implementer-router.ts—lazyno longer conditionally re-wraps the loader itself; it buildsnew Lazy({ loader: async () => ({ default: this.router(router) }), meta: {} }), so every load augments middlewares when present and always applieswithHiddenRouterContract(router, this.contract).implementer-router.test.ts—.lazytests now assert deferred loading, a single loader call, identity preservation, andgetHiddenRouterContract(applied) === contract, in both the plain and with-middlewares suites.
I traced the mechanism end to end: walkProcedureContractsSync swaps the loaded router for the hidden contract, so the RPC/OpenAPI matchers now index contract procedures and route through createContractProcedure, which overrides errorMap/meta/metaPlugins while keeping orderedMiddlewares intact (middlewares still apply). The .lazy variant collapsing to this.router(...) also matches the builder's .lazy, which already does this.
I confirmed the updated assertions genuinely fail on main (reverting only the lazy body yields expected undefined to be { ... }), and that packages/server + packages/openapi (1022 tests), packages/nest (96 tests), and @orpc/server type:check all pass.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Routers implemented with
implement(contract).lazy(...)now follow the contract's error map and meta, the same as routers passed to.router(...). Before this, the loaded router had no contract attached, so its procedures used the implementation's own error map and meta. For example, an error the contract never declares, such asINTERNAL_DEBUGwith a{ sql }payload, reached clients as a defined, typed error.Fixes
{ sub: implement(contract).lazy(() => import('./impl')) }now has errors and meta the contract does not declare stripped, as.router(...)already did..use(...)still apply to lazily loaded routers.implement(contract).router(...)was never affected, and it behaves as before.Testing
.lazytests fail onmainand pass with the fix.packages/server,packages/openapiandpackages/nesttests pass.