fix(rpc,openapi): prevent lazy router races under concurrent requests - #1734
Merged
Conversation
Concurrent match() calls iterated the same pendingRouters array with awaits inside the loop, then reassigned this.pendingRouters wholesale. This caused duplicate loader invocations and, with a lazy router nested inside another lazy router, could drop pending routers entirely (permanent 404). Resolve pending routers one at a time via a memoized init promise shared by concurrent requests, and remove each entry in place only after init completes.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/client
@orpc/contract
@orpc/experimental-durable-iterator
@orpc/hey-api
@orpc/interop
@orpc/json-schema
@orpc/nest
@orpc/openapi
@orpc/openapi-client
@orpc/otel
@orpc/experimental-pino
@orpc/experimental-publisher
@orpc/experimental-publisher-durable-object
@orpc/experimental-ratelimit
@orpc/react
@orpc/react-query
@orpc/experimental-react-swr
@orpc/server
@orpc/shared
@orpc/solid-query
@orpc/standard-server
@orpc/standard-server-aws-lambda
@orpc/standard-server-fastify
@orpc/standard-server-fetch
@orpc/standard-server-node
@orpc/standard-server-peer
@orpc/svelte-query
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/vue-colada
@orpc/vue-query
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — fixes a race condition in lazy router resolution under concurrent requests by replacing the for...of + wholesale array reassignment with a one-at-a-time resolution + promise-memoization pattern.
- Replace
for...of+ array rebuild withwhile+find+ await-one — resolves pending routers one at a time and re-scans after each, so nested lazy routers pushed duringinit()are picked up by the same request instead of being dropped. - Memoize init via
initPromiseon the pending entry — concurrent requests share a singleunlazy()+init()invocation via??=; duplicate loader calls and double route registrations are eliminated. - Splice in place after init completes — pending routers are removed from the array only after resolution, so arriving requests wait on the shared promise rather than missing the route.
- Clear
initPromiseon failure for retry — setspendingRouter.initPromise = undefinedin the.catch()handler, preserving the previous retry-on-next-request behavior. - Add
readonlytopendingRouters— prevents the old wholesale-reassignment pattern at the type level. - Add 3 new concurrency + retry tests to each matcher — covers nested lazy + concurrent requests, multi-branch concurrent matching with 3 nesting levels, and retry after loader failure. All fail against the old implementation and pass with the fix.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Problem
When multiple requests hit the server at the same time while a lazy router was still loading, the matchers (
StandardRPCMatcherandStandardOpenAPIMatcher) could:Fix
Concurrent requests now share a single load per lazy router: each loader runs exactly once, requests arriving mid-load wait for it instead of reloading or missing the route, and nested lazy routers always resolve correctly. If a loader fails, the request rejects and the next request retries as before.
Tests
New tests in both matchers cover concurrent requests to lazy routers, lazy routers nested inside lazy routers (up to three levels), multiple lazy branches matched in parallel, and retry after loader failure. All of them fail on the previous implementation. Full
serverandopenapisuites pass (808 tests).