perf(nest): create the standard handler once instead of per request by dinwwwh · Pull Request #2291 · middleapi/orpc · GitHub
Skip to content

perf(nest): create the standard handler once instead of per request - #2291

Merged
dinwwwh merged 5 commits into
mainfrom
claude/beautiful-noether-dilxdi
Oct 21, 2026
Merged

dinwwwh merged 5 commits into
mainfrom
claude/beautiful-noether-dilxdi

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

ImplementInterceptor used to build a new StandardHandler on every request, which re-ran every plugin's init (sorting plugins and re-wrapping interceptors) each time. The handler is now created once in the interceptor's constructor and reused across requests.

Changes

  • NestHandlerCodec (private, in implement.ts): extends OpenAPIHandlerCodecCore and implements StandardHandlerCodec. NestJS has already routed the request, so resolveProcedure doesn't match the URL. It reads the procedure and the NestJS route params from a private symbol key on the handler context.
  • Per-request match: the interceptor attaches the match with codec.withMatch(context, { procedure, params }). This copies the resolved context and never mutates the one from the module config, so concurrent requests can't interfere.
  • Behavior change: plugins' init now runs once at app startup instead of on every request. A plugin whose init throws now fails startup instead of failing each request.

Why a context symbol rather than AsyncLocalStorage

  • Performance on Node < 24: AsyncLocalStorage relies on promise hooks there. Once it has run once, it slows every promise in the process; a micro-benchmark of 3M awaits on Node 22 went from 186ms to 552ms. Node 24+ (AsyncContextFrame) doesn't have this cost.
  • Fragility: its store is lost without any error when a plugin defers next through another async context.
  • Precedent: RethrowHandlerPlugin and HibernationHandlerPlugin already pass per-request state through a symbol key in the context.
  • Trade-off: the symbol key is visible at runtime in the context procedures receive. A routing interceptor that replaces the context without spreading it drops the match and gets the existing "unmatched result" error.

Tests

  • New test: a routing interceptor that replaces the context without spreading it gets a 500, and the procedure never runs.
  • packages/nest/src stays at 100% coverage.

🤖 Generated with Claude Code

https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj

…d codec

`ImplementInterceptor` used to build a new `StandardHandler` (and re-init
every plugin) on each request. It now builds the handler once, with a
`NestHandlerCodec` that resolves the procedure and NestJS route params
stored in AsyncLocalStorage for the current request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj
Move `NestHandlerCodec` into `implement.ts` without exporting it, drop its
standalone unit tests, and cover its no-stored-procedure branch through the
public API with a routing interceptor that calls `next` outside the request's
async context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj
Replace the codec's AsyncLocalStorage with a private symbol key on the
handler context, the same pattern the rethrow and hibernation plugins use.
On Node < 24 an AsyncLocalStorage adds promise-hook overhead to every
promise in the process once it has run, and its store is silently lost
when `next` is deferred through another async context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2VjNZoAn9SFns5RLdN8Dj
@pullfrog

pullfrog Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

@dinwwwh dinwwwh changed the title refactor(nest): extract codec logic into reusable handler perf(nest): create the standard handler once instead of per request Oct 21, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 21, 2026

Copy link
Copy Markdown

Approvability

Verdict: Approved at 92c5a71

Macroscope's review found this PR approvable — This is a focused Nest integration refactor that reuses one configured StandardHandler and moves procedure resolution into a private codec while preserving the existing request pipeline. The changed context-propagation edge case is covered by an integration test, with no schema, security, billing, or deployment impact.

You can add or adjust custom eligibility rules. Learn more.

@pkg-pr-new

pkg-pr-new Bot commented Oct 21, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@2291

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@2291

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@2291

@orpc/client

npm i https://pkg.pr.new/@orpc/client@2291

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@2291

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@2291

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@2291

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@2291

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@2291

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@2291

@orpc/experimental-lock

npm i https://pkg.pr.new/@orpc/experimental-lock@2291

@orpc/mcp

npm i https://pkg.pr.new/@orpc/mcp@2291

@orpc/msw

npm i https://pkg.pr.new/@orpc/msw@2291

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@2291

@orpc/next

npm i https://pkg.pr.new/@orpc/next@2291

@orpc/node

npm i https://pkg.pr.new/@orpc/node@2291

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@2291

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@2291

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@2291

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@2291

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@2291

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@2291

@orpc/server

npm i https://pkg.pr.new/@orpc/server@2291

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@2291

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@2291

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@2291

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@2291

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@2291

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@2291

commit: 92c5a71

@codecov

codecov Bot commented Oct 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@dinwwwh
dinwwwh merged commit a762db7 into main Oct 21, 2026
11 of 12 checks passed
@codspeed

codspeed Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants