feat(server): expose not-yet-validated input fields to middleware between input schemas - #1960
Conversation
…ween input schemas A middleware placed between multiple input schemas now receives the fields validated so far merged over the raw rest of the input, so whole-input middlewares like caching key on the complete input instead of a partial one. Only the middleware sees this view: the chain continues from the validated value, so remaining schemas still validate those fields and the handler never receives a field no schema validated.
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-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: |
There was a problem hiding this comment.
Pull request overview
Updates server middleware input handling so middleware between schemas sees validated fields merged over remaining raw input.
Changes:
- Merges partial validation results with raw input for intermediate middleware.
- Adds coverage for middleware positions, coercion, unknown fields, and identity.
- Documents validation and trust boundaries.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
packages/server/src/procedure-client.ts |
Builds the intermediate middleware input view. |
packages/server/src/procedure-client.test.ts |
Tests input behavior throughout the schema chain. |
apps/content/docs/procedure.mdx |
Explains multiple-schema middleware behavior. |
apps/content/docs/middleware.mdx |
Warns that unvalidated fields remain untrusted. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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 critical issues — one rough edge and a small doc thought worth a look.
Reviewed changes
packages/server/src/procedure-client.ts— introduced amiddlewareInputalongsidecurrentInputinexecuteProcedureInternal'snext(). For a middleware seated between input schemas (endInputIndex > 0 && < inputSchemas.length) it passesmergeTwoLevels(options.input, currentInput)(validated fields win over raw via the merge's "second wins" semantics), so whole-input middlewares like caching now see the complete input at any point in the chain. Leading middlewares keep the raw input by reference (no clone); middlewares after the last schema and the handler keep the fully validated input.packages/server/src/procedure-client.test.ts— new test covers all three positions (before/between/after), proving raw pre-coercion values (page: '2'), nested/unknown raw rest, validated-wins (id__IDfrom the transform vs rawID), and leading identity; plus the updated existing test at line 626 now assertsparentMidsees the raw rest.apps/content/docs/procedure.mdx/apps/content/docs/middleware.mdx— updated the multiple-schemas description and added a:::dangernote that only fields validated before the middleware are trustworthy.
The guard for the three positions is correct, and the test's between-middleware assertion is the discriminating one: it would fail if the feature were absent (the three remaining schemas' raw rest would be stripped) or if the merge direction were reversed.
ℹ️ Nitpicks
- The merged "rest" is not a defensive copy:
mergeTwoLevelsdeep-merges only one level, so a raw-only nested sub-object survives into the merged view by reference to the originaloptions.input. A between-schemas middleware that mutates such a value writes through to the raw input that later schemas validate (procedure-client.ts:233). Fine for the read-only caching use case, just worth noting it's a shared view, not a prison. - The
before/afterposition assertions in the new test don't by themselves discriminate this feature (they'd pass unchanged on the old code); all the discriminating weight sits on thebetweenMidassertion — which does hold up. No change needed, just noting the coverage is effectively single-assertion.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
| } | ||
|
|
||
| middlewareInput = endInputIndex > 0 && endInputIndex < inputSchemas.length | ||
| ? mergeTwoLevels(options.input, currentInput) |
There was a problem hiding this comment.
Rough edge on the merged view's aliasing: mergeTwoLevels merges only one level, so a raw-only nested/primitive object (a field under params-level that the validated view doesn't redefine) survives into this merged value by reference to options.input. A between-schemas middleware that patches its received input would silently drop mutations to already-validated fields but write through raw-nested mutations into options.input, which later schemas validate directly (line 235). Since the intended use (caching) is read-only it's harmless, but a one-line addition to the middleware.mdx danger note — "the raw rest is shared with the original input, not a defensive copy" — would keep readers out of that foot-gun. The alternative (deep-cloning the merged view) would defeat the same-reference guarantee for the leading-middleware position, so I'd prefer just documenting it.

A middleware placed between multiple
.input()schemas previously received only the fields validated by the schemas before it, so whole-input middlewares like caching built keys from a partial input and returned wrong cached results for distinct requests. Such middlewares now receive the validated fields merged over the raw rest of the input, so they see the complete input anywhere in the chain.Fixes
.input()still get the raw input untouched (same reference, no clone), and middlewares after the last one plus the handler still see only fully validated input, with fields no schema defines dropped.Docs
docs/procedure(Multiple Schemas) anddocs/middleware(Middleware Input) document the new behavior, with a danger note: only trust fields validated before the middleware and treat the rest as untrusted client input.Testing