Skip to content

feat(server): implement automatic error map merging for middleware #924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

QzCurious
Copy link

@QzCurious QzCurious commented Aug 24, 2025

Changes of the PR

  • Add errorMap property to DecoratedMiddleware interface
  • Automatically merge middleware error maps when using .use() method
  • Update Builder and DecoratedProcedure to handle error map merging
  • Add comprehensive tests for error middleware patterns
  • Maintain backward compatibility with existing middleware usage

This change ensures that errors thrown from middleware are properly recognized as "defined" errors, fixing the error handling issue where middleware errors were not being properly typed or handled.


I want #757 (comment) to be implement so I give it a try.

A simple snippet I used to test it:

import { createSafeClient, onError } from "@orpc/client";
import { createRouterClient, os } from "@orpc/server";

const middleware = os
  .errors({
    UNAUTHORIZED: {},
  })
  .middleware(async ({ next, context, errors }, input) => {
    throw errors.UNAUTHORIZED();
  })

const router = os.use(middleware).handler(async () => {});

const client = createSafeClient(
  createRouterClient(router, {
    interceptors: [
      onError((error) =>
        console.dir(error, {
          depth: Infinity,
          colors: true,
        })
      ),
    ],
  })
);

client();

It logs:

{ [Error: Unauthorized] defined: false, code: 'UNAUTHORIZED', status: 401, data: undefined }

This PR make defined to be true

Soem notes

I found that type of middleware already know errors but not really use it at the time. I tried not to touch too much not-directly-related-code and I think the best spot is to store errors in DecoratedMiddleware. As the author want middleware to be clean. And thank to this, the implementation is not affecting too much logical structure.

packages/server/tests/error-middleware-pattern.test.ts is a simple test to state out how thing should work before and after.

Because the test strictly want inline middleware to be as is

it('without map input', () => {
const mid2 = vi.fn()
const applied = builder.use(mid2)
expect(applied).instanceOf(Builder)
expect(applied).not.toBe(builder)
expect(applied['~orpc']).toEqual({
...def,
middlewares: [mid, mid2],
})
})

It took me some time to make the code work and be pretty. I introduce a AnyDecoratedMiddleware to help with it.

https://github.com/QzCurious/orpc/blob/d558620109300ba41f04984fe1713b9184d4ea7f/packages/server/src/builder.ts#L192-L212

Summary by CodeRabbit

  • New Features

    • Use decorated and plain middlewares interchangeably in server builders and procedures.
    • Decorated middlewares may carry optional error maps; error maps are preserved and merged when mapping, concatenating, or composing middlewares, improving cross-stack error handling.
  • Tests

    • Added tests validating error-middleware patterns, automatic error-map propagation into routers, and merged error cases from middleware and router.

- Add errorMap property to DecoratedMiddleware interface
- Automatically merge middleware error maps when using .use() method
- Update Builder and DecoratedProcedure to handle error map merging
- Add comprehensive tests for error middleware patterns
- Maintain backward compatibility with existing middleware usage

This change ensures that errors thrown from middleware are properly recognized
as "defined" errors, fixing the error handling issue where middleware errors
were not being properly typed or handled.
Copy link

vercel bot commented Aug 24, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
orpc Ready Ready Preview Comment Aug 24, 2025 7:44am

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 24, 2025
Copy link

coderabbitai bot commented Aug 24, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds error-map propagation to decorated middlewares: decorated instances can carry an optional errorMap, decoration and concat/mapInput preserve/merge error maps, and builder/procedure APIs accept decorated middlewares. Tests updated and new tests added for error-map merging patterns.

Changes

Cohort / File(s) Summary
Decorated middleware & error map propagation
packages/server/src/middleware-decorated.ts
Adds optional errorMap?: ErrorMap to DecoratedMiddleware, exports AnyDecoratedMiddleware, updates decorateMiddleware to accept an optional errorMap and to preserve/attach it; mapInput and concat propagate and merge error maps; concat accepts `AnyMiddleware
Builder updates for decorated middlewares
packages/server/src/builder.ts
use() now accepts `AnyMiddleware
Procedure updates for decorated middlewares
packages/server/src/procedure-decorated.ts
DecoratedProcedure.use() accepts `AnyMiddleware
Existing test adjustment
packages/server/src/builder.test.ts
Updated expectation: decorateMiddleware is invoked with (mid, def.errorMap) (two args) rather than only (mid).
New tests for error-map behavior
packages/server/tests/error-middleware-pattern.test.ts
Adds three tests validating error-map merging and runtime error handling patterns (base pattern, auto-merge into router, merging errors from multiple sources), asserting error codes and defined flags.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer
  participant Builder as Builder
  participant Merge as mergeErrorMap
  participant Decorate as decorateMiddleware

  Dev->>Builder: use(middleware)
  alt middleware.errorMap present
    Builder->>Merge: merge(builder.errorMap, middleware.errorMap)
    Merge-->>Builder: mergedErrorMap
    Builder->>Decorate: decorateMiddleware(middleware, mergedErrorMap)
    Decorate-->>Builder: DecoratedMiddleware{errorMap: mergedErrorMap}
    Builder-->>Dev: New Builder (with merged errorMap)
  else no middleware.errorMap
    Builder->>Decorate: decorateMiddleware(middleware, builder.errorMap)
    Decorate-->>Builder: DecoratedMiddleware
    Builder-->>Dev: New Builder (errorMap unchanged/omitted)
  end
Loading
sequenceDiagram
  autonumber
  actor Dev as Developer
  participant Proc as DecoratedProcedure
  participant Merge as mergeErrorMap
  participant Decorate as decorateMiddleware

  Dev->>Proc: use(middleware)
  alt middleware.errorMap present
    Proc->>Merge: merge(proc.errorMap, middleware.errorMap)
    Merge-->>Proc: mergedErrorMap
    Proc->>Decorate: decorateMiddleware(middleware, mergedErrorMap)
    Decorate-->>Proc: DecoratedMiddleware{errorMap: mergedErrorMap}
    Proc-->>Dev: DecoratedProcedure (with merged errorMap)
  else no middleware.errorMap
    Proc->>Decorate: decorateMiddleware(middleware, proc.errorMap)
    Decorate-->>Proc: DecoratedMiddleware
    Proc-->>Dev: DecoratedProcedure (middlewares updated, no errorMap merge)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

size:XL

Poem

In burrows of bytes I softly tread,
I stitch error maps where routes are led.
Middlewares hop, their maps entwine—
UNAUTHORIZED, NOT_FOUND, each finds a sign.
Thump-thump—tests cheer; the warren's fine! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @QzCurious, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces automatic merging of error maps from middleware into the main application or procedure's error map. Previously, errors thrown from middleware might not have been correctly identified as "defined" errors within the ORPC framework. This change enhances the error handling mechanism by ensuring that all errors defined within middleware are properly recognized and typed, improving consistency and debugging for developers.

Highlights

  • Automatic Error Map Merging: Implemented a mechanism to automatically merge error maps defined within middleware when they are used with Builder or DecoratedProcedure's .use() method. This ensures that errors originating from middleware are correctly typed and recognized as "defined" errors.
  • Enhanced DecoratedMiddleware: The DecoratedMiddleware interface now includes an errorMap property, allowing middleware to carry its own set of defined errors.
  • Updated Middleware Decoration and Concatenation: The decorateMiddleware function and the concat method for decorated middleware now correctly propagate and merge error maps, maintaining the integrity of error definitions across chained middleware.
  • Comprehensive Error Handling Tests: A new test file, error-middleware-pattern.test.ts, has been added to validate the new error merging behavior and ensure proper error recognition in various middleware usage scenarios.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request successfully implements automatic error map merging for middleware, which is a great enhancement for error handling. The changes are well-structured, and the addition of comprehensive tests in error-middleware-pattern.test.ts is excellent for ensuring correctness. I have a few suggestions to improve maintainability by refactoring some duplicated code and a high-severity suggestion to fix a potential bug where an existing errorMap on a middleware could be lost.

Comment on lines +101 to +104
// Attach error map if provided
if (errorMap) {
decorated.errorMap = errorMap
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The decorateMiddleware function doesn't preserve the errorMap if the passed middleware is already a DecoratedMiddleware and no new errorMap is provided. This can lead to the errorMap being lost when chaining methods like .mapInput(). To fix this, you should check for an existing errorMap on the middleware and preserve it.

  // Attach error map if provided, preserving existing one if present
  if (errorMap) {
    decorated.errorMap = errorMap
  } else if ('errorMap' in middleware && (middleware as AnyDecoratedMiddleware).errorMap) {
    decorated.errorMap = (middleware as AnyDecoratedMiddleware).errorMap
  }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this? I want it typed.

export function decorateMiddleware<
  TInContext extends Context,
  TOutContext extends Context,
  TInput,
  TOutput,
  TErrorConstructorMap extends ORPCErrorConstructorMap<any>,
  TMeta extends Meta,
>(
  middleware: Middleware<TInContext, TOutContext, TInput, TOutput, TErrorConstructorMap, TMeta>
            | DecoratedMiddleware<TInContext, TOutContext, TInput, TOutput, TErrorConstructorMap, TMeta>,
  errorMap?: ErrorMap,
): DecoratedMiddleware<TInContext, TOutContext, TInput, TOutput, TErrorConstructorMap, TMeta> {
  const decorated = ((...args) => middleware(...args)) as DecoratedMiddleware<TInContext, TOutContext, TInput, TOutput, TErrorConstructorMap, TMeta>

  // Attach error map if provided
  if (errorMap) {
    decorated.errorMap = errorMap
  }
  if ('errorMap' in middleware) {
    decorated.errorMap = middleware.errorMap
  }

Comment on lines +200 to 211
if (!('errorMap' in middleware) || !middleware.errorMap) {
return new Builder({
...this['~orpc'],
middlewares: addMiddleware(this['~orpc'].middlewares, mapped),
}) as any
}

return new Builder({
...this['~orpc'],
middlewares: addMiddleware(this['~orpc'].middlewares, mapped),
errorMap: mergeErrorMap(this['~orpc'].errorMap, middleware.errorMap),
}) as any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic inside the use method has some code duplication between the if and the else path. You can refactor it to be more concise and maintainable by creating the new builder definition object and conditionally adding the errorMap.

    const errorMap = ('errorMap' in middleware && middleware.errorMap)
      ? mergeErrorMap(this['~orpc'].errorMap, middleware.errorMap)
      : this['~orpc'].errorMap

    return new Builder({
      ...this['~orpc'],
      middlewares: addMiddleware(this['~orpc'].middlewares, mapped),
      errorMap,
    }) as any

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's about whether the "errorMap" exist or not. The given solution is conciser but would make the object {errorMap: undefined} instead of omit the "errorMap" property. I don't know if it would affect anytime so I better make it safer.

Comment on lines +145 to 156
if (!('errorMap' in middleware) || !middleware.errorMap) {
return new DecoratedProcedure({
...this['~orpc'],
middlewares: addMiddleware(this['~orpc'].middlewares, mapped),
})
}

return new DecoratedProcedure({
...this['~orpc'],
middlewares: addMiddleware(this['~orpc'].middlewares, mapped),
errorMap: mergeErrorMap(this['~orpc'].errorMap, middleware.errorMap),
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the use method in builder.ts, this implementation has duplicated code. It can be refactored for better readability and maintainability.

    const errorMap = ('errorMap' in middleware && middleware.errorMap)
      ? mergeErrorMap(this['~orpc'].errorMap, middleware.errorMap)
      : this['~orpc'].errorMap

    return new DecoratedProcedure({
      ...this['~orpc'],
      middlewares: addMiddleware(this['~orpc'].middlewares, mapped),
      errorMap,
    })

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (9)
packages/server/src/middleware-decorated.ts (3)

101-105: Make internal errorMap non-enumerable to avoid leaking internal details

Since errorMap is marked @internal and attached to a function object, setting it as a non-enumerable property prevents accidental exposure during object spreads or logging.

Apply:

-  // Attach error map if provided
-  if (errorMap) {
-    decorated.errorMap = errorMap
-  }
+  // Attach error map if provided (keep internal)
+  if (errorMap) {
+    Object.defineProperty(decorated, 'errorMap', {
+      value: errorMap,
+      enumerable: false,
+      configurable: true,
+      writable: true,
+    })
+  }

115-118: Preserve concat-middleware’s errorMap on the inner mapped wrapper as well

When mapInput is provided, decorateMiddleware(concatMiddleware) drops any existing errorMap for the inner mapped wrapper. You already pass the combined map to the outer decoration, so behavior is correct; however, preserving the inner wrapper’s errorMap improves debuggability and symmetry with mapInput above.

-  const mapped = mapInput
-    ? decorateMiddleware(concatMiddleware).mapInput(mapInput)
+  const mapped = mapInput
+    ? decorateMiddleware(
+        concatMiddleware,
+        'errorMap' in concatMiddleware ? concatMiddleware.errorMap : undefined,
+      ).mapInput(mapInput)
     : concatMiddleware

120-124: Use contract’s mergeErrorMap helper for consistency

You’re manually spreading error maps here while other places use mergeErrorMap. Prefer the helper to centralize semantics (in case merge rules evolve).

-  const combinedErrorMap = {
-    ...decorated.errorMap,
-    ...('errorMap' in concatMiddleware ? concatMiddleware.errorMap : undefined),
-  }
+  const combinedErrorMap = mergeErrorMap(
+    (decorated as AnyDecoratedMiddleware).errorMap ?? {},
+    ('errorMap' in concatMiddleware ? concatMiddleware.errorMap : undefined) ?? {},
+  )

Note: import mergeErrorMap from @orpc/contract at the top of this file.

packages/server/src/procedure-decorated.ts (2)

140-144: Optionally preserve middleware.errorMap on the mapped wrapper

Similar to middleware-decorated.concat, when mapInput is provided you re-wrap the middleware without carrying its errorMap. Functionally fine since you merge into the procedure’s errorMap below; preserving it on the wrapper can aid debugging.

-  const mapped = mapInput
-    ? decorateMiddleware(middleware).mapInput(mapInput)
+  const mapped = mapInput
+    ? decorateMiddleware(
+        middleware as AnyDecoratedMiddleware,
+        'errorMap' in middleware ? middleware.errorMap : undefined,
+      ).mapInput(mapInput)
     : middleware

152-156: Merging procedure errorMap with middleware.errorMap is correct and order-safe

mergeErrorMap(existing, incoming) gives precedence to the middleware’s definitions on key conflicts, which matches expectations. Consider adding a regression test where keys collide to document this precedence.

I can add a focused test to assert collision precedence if desired.

packages/server/tests/error-middleware-pattern.test.ts (2)

48-79: Add a concat-path test to exercise decorated.concat merging

Current tests cover builder.use and base errors. Consider adding a case where two decorated middlewares are concatenated (with and without mapInput) to ensure combined errorMap flows through concat, not just use.

Here’s a minimal pattern you can adapt:

+  it('should merge error maps through decorated.concat (with and without mapInput)', async () => {
+    const mwA = os.errors({ UNAUTHORIZED: {} }).middleware(async ({ next }) => next({}))
+    const mwB = os.errors({ NOT_FOUND: {} }).middleware(async ({ next }) => next({}))
+
+    // concat without mapInput
+    const combined1 = mwA.concat(mwB)
+
+    // concat with mapInput
+    const combined2 = mwA.concat(mwB, (_: any) => _)
+
+    const r1 = os.use(combined1).handler(async ({ errors }) => { throw errors.NOT_FOUND() })
+    const r2 = os.use(combined2).handler(async ({ errors }) => { throw errors.UNAUTHORIZED() })
+
+    const c1 = createSafeClient(createRouterClient(r1))
+    const c2 = createSafeClient(createRouterClient(r2))
+
+    const [e1,, d1] = await c1()
+    const [e2,, d2] = await c2()
+
+    expect(d1).toBe(true); expect((e1 as any).code).toBe('NOT_FOUND')
+    expect(d2).toBe(true); expect((e2 as any).code).toBe('UNAUTHORIZED')
+  })

63-66: Tracking the inference limitation is good; consider a follow-up issue

The @ts-expect-error notes the current type inference gap for middleware-provided errors. Worth opening an issue to track improving ErrorMap inference across use/concat boundaries.

I can draft a proposal for propagating middleware ErrorMap types through Builder/Procedure generics if helpful.

packages/server/src/builder.ts (2)

192-205: use: mapping path drops middleware.errorMap on the wrapper (optional)

As in other sites, when mapInput is present you re-wrap the incoming middleware without carrying its errorMap. Functional behavior remains correct because you merge into the Builder’s errorMap below; preserving it on the wrapper can aid debugging and future extensions.

-  const mapped = mapInput
-    ? decorateMiddleware(middleware).mapInput(mapInput)
+  const mapped = mapInput
+    ? decorateMiddleware(
+        middleware as AnyDecoratedMiddleware,
+        'errorMap' in middleware ? middleware.errorMap : undefined,
+      ).mapInput(mapInput)
     : middleware

200-211: Conditional merge on presence of middleware.errorMap is correct; consider a collision test

Merging only when truthy keeps backward compatibility. Add a unit test where the router and middleware define the same code to document precedence (middleware overwrites router on same key).

I can extend builder.test.ts with a focused case asserting merge precedence.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7f288fb and d558620.

📒 Files selected for processing (5)
  • packages/server/src/builder.test.ts (1 hunks)
  • packages/server/src/builder.ts (3 hunks)
  • packages/server/src/middleware-decorated.ts (5 hunks)
  • packages/server/src/procedure-decorated.ts (2 hunks)
  • packages/server/tests/error-middleware-pattern.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
packages/server/tests/error-middleware-pattern.test.ts (4)
packages/server/src/builder.ts (4)
  • os (344-360)
  • middleware (146-150)
  • errors (158-165)
  • router (325-329)
packages/server/src/procedure-decorated.ts (1)
  • errors (39-53)
packages/client/src/client-safe.ts (1)
  • createSafeClient (24-42)
packages/server/src/router-client.ts (1)
  • createRouterClient (27-71)
packages/server/src/middleware-decorated.ts (3)
packages/contract/src/error.ts (1)
  • ErrorMap (31-33)
packages/server/src/builder.ts (2)
  • middleware (146-150)
  • input (251-259)
packages/server/src/middleware.ts (2)
  • AnyMiddleware (63-63)
  • MapInputMiddleware (65-67)
packages/server/src/procedure-decorated.ts (5)
packages/server/src/builder.ts (1)
  • middleware (146-150)
packages/server/src/middleware.ts (2)
  • AnyMiddleware (63-63)
  • MapInputMiddleware (65-67)
packages/server/src/middleware-decorated.ts (2)
  • AnyDecoratedMiddleware (86-86)
  • decorateMiddleware (88-142)
packages/server/src/middleware-utils.ts (1)
  • addMiddleware (29-31)
packages/contract/src/error.ts (1)
  • mergeErrorMap (37-39)
packages/server/src/builder.ts (4)
packages/server/src/middleware-decorated.ts (2)
  • decorateMiddleware (88-142)
  • AnyDecoratedMiddleware (86-86)
packages/server/src/middleware.ts (2)
  • AnyMiddleware (63-63)
  • MapInputMiddleware (65-67)
packages/server/src/middleware-utils.ts (1)
  • addMiddleware (29-31)
packages/contract/src/error.ts (1)
  • mergeErrorMap (37-39)
🔇 Additional comments (8)
packages/server/src/middleware-decorated.ts (3)

15-20: Good addition: carry errorMap on decorated middlewares

Adding an optional errorMap to DecoratedMiddleware makes the merge story straightforward across mapInput/concat. Looks correct.


106-113: mapInput correctly preserves errorMap

Forwarding decorated.errorMap into the nested decoration keeps the metadata intact after input mapping. Nice.


125-137: Concat composition looks correct; verify context merge order under nesting

The next chaining merges contexts as { ...options.context, ...next1?.context } then later { ...next1?.context, ...next2?.context }. This maintains right-most precedence. Just flagging to double-check against your intended override order for nested middlewares.

Would you like a follow-up test that chains decorated.concat twice (with and without mapInput) and asserts both errorMap merge and context override order?

packages/server/src/builder.test.ts (1)

133-134: Test update matches new decorateMiddleware signature

Expecting decorateMiddleware(mid, def.errorMap) ensures propagation from builder into decoration. Looks good.

packages/server/src/procedure-decorated.ts (2)

15-16: Importing AnyDecoratedMiddleware aligns the public API with decorated inputs

This keeps use flexible without breaking existing plain middleware usage. Good.


140-151: Early return when no errorMap keeps hot path lean

Skipping a merge when middleware.errorMap is falsy is a nice optimization and preserves prior behavior. LGTM.

packages/server/tests/error-middleware-pattern.test.ts (1)

26-46: Great coverage of the new automatic merge behavior

This verifies the key runtime effect: defined=true after using a middleware that contributes its own errorMap. Nice assertion on router['~orpc'].errorMap as well.

packages/server/src/builder.ts (1)

149-150: Passing builder errorMap into decorateMiddleware is the right move

This ensures decorated middlewares created via builder.middleware start with the router’s current errorMap. Matches the test update.

@QzCurious QzCurious force-pushed the feat/make-error-thrown-from-middleware-defined-error branch from adb581a to 1f74bc7 Compare August 24, 2025 07:36
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/server/src/middleware-decorated.ts (1)

33-53: Public type signature of concat doesn’t accept DecoratedMiddleware (but implementation does) — update overloads

Interface overloads still accept only Middleware<…>; however, implementation at Line 119 accepts AnyMiddleware | AnyDecoratedMiddleware. As-is, callers won’t be able to pass a DecoratedMiddleware without casts, defeating the purpose of the change.

Apply this diff to both overloads to accept a decorated input with matching generics:

-  >(
-    middleware: Middleware<
-      UInContext | MergedCurrentContext<TInContext, TOutContext>,
-      UOutContext,
-      UInput,
-      TOutput,
-      TErrorConstructorMap,
-      TMeta
-    >,
+  >(
+    middleware:
+      | Middleware<
+          UInContext | MergedCurrentContext<TInContext, TOutContext>,
+          UOutContext,
+          UInput,
+          TOutput,
+          TErrorConstructorMap,
+          TMeta
+        >
+      | DecoratedMiddleware<
+          UInContext | MergedCurrentContext<TInContext, TOutContext>,
+          UOutContext,
+          UInput,
+          TOutput,
+          TErrorConstructorMap,
+          TMeta
+        >,

And for the mapInput overload:

-  >(
-    middleware: Middleware<
-      UInContext | MergedCurrentContext<TInContext, TOutContext>,
-      UOutContext,
-      UMappedInput,
-      TOutput,
-      TErrorConstructorMap,
-      TMeta
-    >,
+  >(
+    middleware:
+      | Middleware<
+          UInContext | MergedCurrentContext<TInContext, TOutContext>,
+          UOutContext,
+          UMappedInput,
+          TOutput,
+          TErrorConstructorMap,
+          TMeta
+        >
+      | DecoratedMiddleware<
+          UInContext | MergedCurrentContext<TInContext, TOutContext>,
+          UOutContext,
+          UMappedInput,
+          TOutput,
+          TErrorConstructorMap,
+          TMeta
+        >,

Also applies to: 61-83

♻️ Duplicate comments (1)
packages/server/src/middleware-decorated.ts (1)

96-109: Bug: Provided errorMap is overwritten by middleware.errorMap — merge instead with clear precedence

Current sequence sets decorated.errorMap = errorMap, then unconditionally overwrites it with middleware.errorMap if present, dropping the router/global map passed from Builder. Merge both; typically the middleware’s own map should override defaults on key collision.

Apply:

-  // Attach error map if provided
-  if (errorMap) {
-    decorated.errorMap = errorMap
-  }
-  if ('errorMap' in middleware) {
-    decorated.errorMap = middleware.errorMap
-  }
+  // Merge provided errorMap with any existing one on the middleware.
+  // Precedence: middleware.errorMap overrides passed-in defaults on key collision.
+  const incoming = errorMap
+  const existing = ('errorMap' in middleware ? (middleware as AnyDecoratedMiddleware).errorMap : undefined)
+  const merged = existing || incoming ? { ...(incoming ?? {}), ...(existing ?? {}) } : undefined
+  if (merged) {
+    decorated.errorMap = merged
+  }
🧹 Nitpick comments (4)
packages/server/src/middleware-decorated.ts (4)

15-20: Nice: DecoratedMiddleware now carries an optional errorMap

Clear doc and scoping as internal. Consider documenting precedence semantics (who wins on key collision) in concat/mapInput JSDoc for future readers.


119-128: Safer merge for combinedErrorMap and clarify precedence

Spreading possibly undefined is brittle and the current code relies on JS semantics that can be surprising. Also, consider typing the result explicitly and reusing the same precedence rule (concatMiddleware’s map overrides earlier).

Apply:

-  const combinedErrorMap = {
-    ...decorated.errorMap,
-    ...('errorMap' in concatMiddleware ? concatMiddleware.errorMap : undefined),
-  }
+  const left = decorated.errorMap
+  const right = ('errorMap' in concatMiddleware ? (concatMiddleware as AnyDecoratedMiddleware).errorMap : undefined)
+  const combinedErrorMap: ErrorMap | undefined =
+    left || right ? { ...(left ?? {}), ...(right ?? {}) } : undefined

129-141: Minor: simplify next() parameter handling for readability

Destructuring into arrays for single optional params is unconventional and obscures intent. Optional params are clearer.

Apply:

-    const concatted = decorateMiddleware((options, input, output, ...rest) => {
-      const merged = middleware({
-        ...options,
-        next: (...[nextOptions1]: [any]) => mapped({
-          ...options,
-          context: { ...options.context, ...nextOptions1?.context },
-          next: (...[nextOptions2]) => options.next({ context: { ...nextOptions1?.context, ...nextOptions2?.context } }) as any,
-        }, input, output, ...rest),
-      } as any, input as any, output as any, ...rest)
+    const concatted = decorateMiddleware((options, input, output, ...rest) => {
+      const merged = middleware({
+        ...options,
+        next: (nextOptions1?: any) =>
+          mapped(
+            {
+              ...options,
+              context: { ...options.context, ...nextOptions1?.context },
+              next: (nextOptions2?: any) =>
+                options.next({ context: { ...nextOptions1?.context, ...nextOptions2?.context } }) as any,
+            },
+            input,
+            output,
+            ...rest,
+          ),
+      } as any, input as any, output as any, ...rest)

15-20: Request: document and test error-map precedence rules

Please add a short note in code (JSDoc) and a test asserting that when both router-level and middleware-level maps provide the same code, the middleware-level mapping wins (and similarly in concat, the later middleware’s mapping wins).

I can draft an additional test in packages/server/tests/error-middleware-pattern.test.ts that asserts precedence across: builder → middleware → concat(middleware2). Let me know if you want me to push that.

Also applies to: 96-109, 119-128

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d558620 and 1f74bc7.

📒 Files selected for processing (1)
  • packages/server/src/middleware-decorated.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/server/src/middleware-decorated.ts (3)
packages/contract/src/error.ts (1)
  • ErrorMap (31-33)
packages/server/src/builder.ts (2)
  • middleware (146-150)
  • input (251-259)
packages/server/src/middleware.ts (3)
  • Middleware (46-61)
  • AnyMiddleware (63-63)
  • MapInputMiddleware (65-67)
🔇 Additional comments (4)
packages/server/src/middleware-decorated.ts (4)

1-1: Import looks correct

Bringing in ErrorMap from @orpc/contract is needed for the new property usage.


86-87: Good addition: AnyDecoratedMiddleware alias

This will simplify Builder/Procedure typing and unions.


110-117: mapInput correctly preserves the errorMap

Passing decorated.errorMap through decorateMiddleware keeps propagation intact across mapping.


140-141: LGTM: Passing combinedErrorMap to outer decorateMiddleware

Ensures merged maps persist when chaining further.

* Error map associated with this middleware (if any)
* @internal
*/
errorMap?: ErrorMap
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your current implementation seem not "typesafe" at all, it only cover at runtime level while ignore the type level.

To fully support this, we require procedure inherit middleware's errors on both runtime and type levels when .use

And you should change the type of Middleware instead of DecoratedMiddleware

Copy link
Author

@QzCurious QzCurious Aug 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For typesafe, do you mean we should be able to infer the errors type in handler? I do understand my implementation is not fulfilling it. But I want to fix the "defined for error thrown from middleware is false" thing first without affecting current behavior.

https://github.com/QzCurious/orpc/blob/d558620109300ba41f04984fe1713b9184d4ea7f/packages/server/tests/error-middleware-pattern.test.ts#L48-L71

Screen.Recording.2025-08-24.at.17.mp4

If we want it work as a whole, that is, both "defined=true" and "typesafe", I agree change Middleware would be the right way to it. Then, do you mind if I store the errors as a property of a middleware?
I tried to change type of Middleware as I did with DecoratedMiddleware but it could affect more places. Wish I can make it well.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You must make sure runtime errors and type errors are matching, if only merging at runtime the client can throw defined error more than the type describe -> unexpected behavior.

Fore reference here my implementation in the past: #224 - but it faces some issue with contract-first

@unnoq unnoq marked this pull request as draft August 24, 2025 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size:L This PR changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants