-
-
Notifications
You must be signed in to change notification settings - Fork 102
feat(nest): Change the nest integration to support Nest features that were competing with Orpc #1149
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
base: main
Are you sure you want to change the base?
feat(nest): Change the nest integration to support Nest features that were competing with Orpc #1149
Conversation
|
@Mathiasduc is attempting to deploy a commit to the unnoq-team Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughThis PR enhances the Nest integration with standardized error handling and response utilities. It introduces ORPCExceptionFilter for converting ORPCError instances into oRPC-formatted responses, adds utility functions for setting HTTP responses across Fastify and Node adapters, refactors the request handler implementation to separate error concerns, and includes comprehensive test coverage for response types and Nest features. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant NestApp as NestJS App
participant Filter as ORPCExceptionFilter
participant Encoder as StandardOpenAPICodec
participant Server as StandardServerNode/Fastify
participant Response as HTTP Response
Client->>NestApp: Send ORPC Request
NestApp->>NestApp: Decode Input (fails)
NestApp->>Filter: Throw ORPCError
Filter->>Encoder: Encode ORPCError
Encoder-->>Filter: Encoded Error Response
Filter->>Server: Send Standard Response
Server->>Response: Set Status & Headers
Server->>Response: Write Body
Response-->>Client: HTTP Response
sequenceDiagram
participant Handler as ORPC Handler
participant Utils as setStandardNodeResponse
participant Parser as getStandardHttpBodyAndHeaders
participant HTTP as Node HTTP Response
Handler->>Handler: Execute & Get Result
Handler->>Utils: setStandardNodeResponse(res, standardResponse)
Utils->>Parser: getStandardHttpBodyAndHeaders(body, headers)
alt Body is Blob
Parser->>Parser: Create Readable Stream
else Body is FormData
Parser->>Parser: Convert via Response API
else Body is AsyncIterator
Parser->>Parser: Format as Event Stream
else Other
Parser->>Parser: Stringify & Format
end
Parser-->>Utils: { body, headers }
Utils->>HTTP: Set Status Code
Utils->>HTTP: Set Headers
Utils->>HTTP: Write/Pipe Body
HTTP-->>Handler: Complete
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20–25 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
|
Hi @unnoq This is the PR we were talking about in #992 . |
6892585 to
8366106
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don’t understand why we need to use return just to increase compatibility with NestJS. Modifying the response body is an anti-pattern and makes it incompatible with the generated openapi spec. Why not modify the headers instead? We can already change headers without requiring a return.
packages/nest/src/implement.ts
Outdated
|
|
||
| const client = createProcedureClient(procedure, { | ||
| ...this.config, | ||
| context: contextWithRequest, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This approach is not typesafe. While implement expects an empty context {}, this can lead to problems. For instance, a middleware might expect a context like { request?: number | undefined }. Because an empty object {} doesn't conflict can satisfy middleware's dependent-context so we can .use when implement. But in this case request is req -> middleware can run failed because middleware expect request is undefined or number
| * @param headers - WARNING: The headers can be mutated by the function and may affect the original headers. | ||
| * @param options | ||
| */ | ||
| export function toResponseBody( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this package needs these APIs. I believe we should write it inside the nest package instead. We can implement some of them as signal functions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll try to not need to define this function since it's so similar to toNodeHttpBody. Adding an option to stringify the body or not should be enough.
We can implement some of them as signal functions
I did not understand what you meant by that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean you can write a signal function in nest package instead.
| import * as StandardServerFastify from '@orpc/standard-server-fastify' | ||
| import * as StandardServerNode from '@orpc/standard-server-node' | ||
|
|
||
| const codec = new StandardOpenAPICodec( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm looking for a way to make all codecs extendable. Specifically, I'd like to know if it's possible to add a custom serializer by passing it into the main configuration, similar to the method described for extending native data types.
packages/nest/src/implement.ts
Outdated
| // Set status and headers | ||
| if ('raw' in res) { | ||
| await StandardServerFastify.sendStandardResponse(res, standardResponse, this.config) | ||
| return StandardServerFastify.setStandardResponse(res as FastifyReply, standardResponse, this.config) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sure you tests there cases:
undefinedin this case expect body is empty + no content-type is setjson datain this case expect returnjson data(not stringify) + content-type is jsonfile/blobin this case expect streaming response + content-type = file.type
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
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-publisher
@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: |
Not from inside a Nest/Express/Fastify middleware if the response has already been sent by Orpc
It is but, I feel like, this should not be a concern of this integration. There is legitimate reasons to modifying a response defined by Orpc and ways to do it so it will not break contract/specs.
This features exist in Orpc eco-system, with plugin or external packages, and users can also implement them at the ORPC level (orpc's middleware/interceptors) but why not make it possible to delegate this features to the platform being integrated with, Nest in this case. But, indeed, it also let the door open to the users to shoot themselves in the foot and break their own contract and specs. |
|
We can move this discussion in an issue if you prefer |
Reasonable, and I agreed - I just don't want mess thing up by introducing new apis, if you do this good enough I have no reason to reject.
I believe without |
After thinking about it more, can you provide real examples or popular npm packages that depend on this? Personally, I believe that under the hood they should be Express middleware or Fastify plugins - so I don't think they need Also, regarding your implementation, we can't always |
| } | ||
| }) | ||
|
|
||
| resBody.once('error', error => res.destroy(error)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Node.js we manually send streaming responses, but in Fastify you return the stream - this creates inconsistent behavior.
| body: StandardBody, | ||
| headers: StandardHeaders, | ||
| options: ToNodeHttpBodyOptions = {}, | ||
| options: ToNodeHttpBodyOptions = { shouldStringifyBody: true }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should create other function or intercept before call toNodeHttpBody, this option is not related to this util + default option like this is not safe (we can easily accidentally disable strintify).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, the default is the existing behaviour so it should not be an issue but I understand the worry. I will extract this change in another function. I'm also finishing on writing a test suite for all the core request type that need to be supported (basically everything in this file.)
I would also like to create another PR with all these tests to demonstrate the limitation of the current implementation vs this one.
|
Hi, I created another PR that add this new tests against the current
You were right, I'm not sure how but this indeed work, even with current implementation. It leave the rest of the failing tests that cannot be supported when sending the response at the Orpc level. |
|
So the compression case do indeed work because the Node/Express version actually patches the res.send method ! 🤯 fastify.addHook('onSend', async (request, reply, payload) => {
// Check if compression should be applied
if (!shouldCompress(request, reply)) {
return payload
}
// Modify content-encoding header
reply.header('content-encoding', 'gzip')
reply.removeHeader('content-length')
// Return compressed payload
if (isStream(payload)) {
return payload.pipe(createGzip())
} else {
return compress(payload)
}
})This is some wild monkeypatching but it works |
I'm hesitant to add Nest.js-specific handling because I'm not familiar with the framework and haven't used it before. I need a concrete reason for this change - based on my understanding, Could you provide a real-world example or point to npm packages that specifically require |
…at were competing with Orpc
…ures that were competing with Orpc
…ures that were competing with Orpc
21850e3 to
f56f7a0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (5)
packages/standard-server-node/src/body.ts (1)
57-57: Consider more explicit documentation about header mutation.While "may affect" is clearer than the previous wording, it's still somewhat ambiguous. Consider being more explicit about when and how headers are mutated.
Apply this diff for clearer documentation:
- * @param headers - WARNING: The headers can be mutated by the function and may affect the original headers. + * @param headers - WARNING: This object is mutated directly. Content-type and content-disposition headers are deleted and then repopulated based on the body type.packages/nest/src/filters/orpc-exception.filter.ts (2)
11-16: Hardcoded codec limits extensibility.The module-level codec instance prevents users from customizing error serialization. A past review comment requested making codecs extendable through configuration.
Consider accepting the codec as a constructor parameter or configuration option:
+import type { StandardCodec } from '@orpc/standard-server' + -const codec = new StandardOpenAPICodec( +const defaultCodec = new StandardOpenAPICodec( new StandardOpenAPISerializer( new StandardOpenAPIJsonSerializer(), new StandardBracketNotationSerializer(), ), ) @Catch(ORPCError) @Injectable() export class ORPCExceptionFilter implements ExceptionFilter { constructor( - private config?: StandardServerNode.SendStandardResponseOptions | undefined, + private options?: { + codec?: StandardCodec + responseOptions?: StandardServerNode.SendStandardResponseOptions + }, ) {} async catch(exception: ORPCError<any, any>, host: ArgumentsHost) { const ctx = host.switchToHttp() const res = ctx.getResponse<Response | FastifyReply>() - const standardResponse = codec.encodeError(exception) + const codec = this.options?.codec ?? defaultCodec + const standardResponse = codec.encodeError(exception) // Send the response directly with proper status and headers const isFastify = 'raw' in res if (isFastify) { - await StandardServerFastify.sendStandardResponse(res as FastifyReply, standardResponse, this.config) + await StandardServerFastify.sendStandardResponse(res as FastifyReply, standardResponse, this.options?.responseOptions) } else { - await StandardServerNode.sendStandardResponse(res as Response, standardResponse, this.config) + await StandardServerNode.sendStandardResponse(res as Response, standardResponse, this.options?.responseOptions) } } }This would allow users to provide custom serializers as mentioned in the extending native data types documentation.
Based on past review comments.
56-62: Consider extracting response dispatch logic.The Fastify/Node detection and response dispatch pattern is duplicated in
implement.ts(lines 169-174). Consider extracting this into a shared utility function.Extract to a helper in
utils.ts:export async function dispatchStandardResponse( res: Response | FastifyReply, standardResponse: StandardResponse, options?: StandardServerNode.SendStandardResponseOptions, ): Promise<void> { const isFastify = 'raw' in res if (isFastify) { await StandardServerFastify.sendStandardResponse(res as FastifyReply, standardResponse, options) } else { await StandardServerNode.sendStandardResponse(res as Response, standardResponse, options) } }Then both the filter and interceptor can use this shared logic.
packages/nest/src/response-types.test.ts (1)
425-460: Loosen SSE assertions to match the spec.
text/event-streamresponses frequently append; charset=utf-8, and the field grammar allows optional whitespace after the colon. The current.expect('Content-Type', 'text/event-stream')and/data: (.+)/patterns will fail or miss events even though oRPC is behaving correctly. Switch to something like.expect('Content-Type', /text\/event-stream/)and/^data:\s*(.+)$/m(or similar) so we acknowledge valid MIME parameters and the optional space defined in the SSE ABNF.packages/nest/src/nest-features.test.ts (1)
400-575: Reset the spy between tests.
sendStandardResponseSpyis defined once for the whole suite; without amockClear()/mockRestore()in the lifecycle hooks, earlier expectations can taint later ones when this file (or others) add more tests. Clearing it inbeforeEach/afterEachkeeps the “not called” assertions deterministic.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (10)
packages/nest/README.md(1 hunks)packages/nest/package.json(1 hunks)packages/nest/src/filters/orpc-exception.filter.ts(1 hunks)packages/nest/src/implement.test.ts(6 hunks)packages/nest/src/implement.ts(2 hunks)packages/nest/src/index.ts(1 hunks)packages/nest/src/nest-features.test.ts(1 hunks)packages/nest/src/response-types.test.ts(1 hunks)packages/nest/src/utils.ts(2 hunks)packages/standard-server-node/src/body.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
packages/nest/src/filters/orpc-exception.filter.ts (4)
packages/openapi/src/adapters/standard/openapi-codec.ts (1)
StandardOpenAPICodec(23-132)packages/nest/src/implement.ts (1)
Injectable(101-178)packages/standard-server-fastify/src/response.ts (1)
SendStandardResponseOptions(6-6)packages/standard-server-node/src/response.ts (1)
SendStandardResponseOptions(6-6)
packages/nest/src/implement.ts (5)
packages/server/src/procedure-client.ts (1)
createProcedureClient(72-165)packages/standard-server/src/types.ts (1)
StandardResponse(34-41)packages/client/src/error.ts (1)
ORPCError(112-169)packages/standard-server/src/utils.ts (1)
flattenHeader(51-61)packages/nest/src/utils.ts (2)
setStandardFastifyResponse(65-80)setStandardNodeResponse(82-119)
packages/nest/src/nest-features.test.ts (3)
packages/contract/src/builder.ts (1)
oc(189-198)packages/nest/src/implement.ts (2)
Implement(38-90)Injectable(101-178)packages/nest/src/filters/orpc-exception.filter.ts (1)
Catch(44-64)
packages/nest/src/response-types.test.ts (3)
packages/contract/src/builder.ts (1)
oc(189-198)packages/nest/src/implement.ts (1)
Implement(38-90)packages/nest/src/nest-features.test.ts (1)
data(252-261)
packages/nest/src/utils.ts (6)
packages/standard-server/src/types.ts (3)
StandardResponse(34-41)StandardBody(5-11)StandardHeaders(1-3)packages/standard-server-node/src/body.ts (1)
ToNodeHttpBodyOptions(53-53)packages/standard-server-node/src/types.ts (1)
NodeHttpResponse(17-17)packages/standard-server/src/utils.ts (2)
flattenHeader(51-61)generateContentDisposition(4-13)packages/shared/src/iterator.ts (1)
isAsyncIteratorObject(6-12)packages/shared/src/json.ts (1)
stringifyJSON(9-12)
🔇 Additional comments (10)
packages/nest/package.json (1)
64-77: LGTM!The new devDependencies are appropriate for testing compression features with both Express and Fastify adapters.
packages/nest/src/utils.ts (2)
65-80: Return semantics change requires clarification.The function returns the body for Fastify replies (line 78) instead of sending the response directly. This is a departure from the previous
sendStandardResponseapproach.Based on the PR discussion, unnoq raised concerns about:
- Streaming cases where handlers cannot always return body
- Inconsistent behavior between streaming and non-streaming responses
Please clarify:
- How does this work with streaming responses (async iterators)?
- What happens to the returned body - does Nest automatically send it?
- Does this approach work consistently across all StandardBody types (undefined, Blob, FormData, URLSearchParams, async iterators, JSON)?
Consider adding integration tests that verify all StandardBody types work correctly with the return approach, especially streaming cases.
161-167: The "double stringify" terminology is misleading—clarify what's actually happening.The code calls
stringifyJSONonce, not twice. When a string body is passed,JSON.stringifycorrectly encodes it by adding quotes (e.g.,"hello"becomes"\"hello\""), which is necessary for valid JSON. However, the comment's wording suggests a workaround or unusual behavior, which is confusing. The implementation is correct, but consider:
- Update the comment to clarify that this is standard JSON encoding of string values, not a workaround
- Add a test case for string bodies to document and validate this behavior
- Remove references to "double stringify," which is inaccurate terminology
packages/nest/src/implement.ts (2)
128-167: Improved error handling structure.The explicit separation of decoding, execution, and encoding phases with targeted error handling is clear and maintainable. Each phase appropriately wraps non-ORPC errors into ORPCError instances.
The approach allows NestJS exception filters to handle all errors uniformly while providing clear error messages for each phase.
169-174: No action required—comprehensive tests confirm the return-based response handling approach is working correctly.Verification found that tests are parameterized to run with both Express and Fastify adapters, which means both
setStandardFastifyResponseandsetStandardNodeResponsebranches are exercised. Test coverage includes all requested StandardBody type variations:
- Empty/undefined responses
- JSON/object responses
- URLSearchParams with proper content-type headers
- FormData with multipart handling
- Blob responses
- AsyncIterable/streaming (SSE) with text/event-stream content-type
The parameterized test structure ensures the
'raw' in rescheck at implement.ts line 169 correctly routes to both response handlers across Express and Fastify, validating the return-based approach works consistently for all response types.packages/nest/src/index.ts (1)
1-1: LGTM!The export appropriately exposes the new
ORPCExceptionFilterfor public use.packages/nest/README.md (1)
71-98: Clear documentation of the new error handling approach.The documentation effectively explains the optional nature of
ORPCExceptionFilterand provides practical examples for different registration methods.Consider adding a note about customizing error serialization once the codec is made configurable (as suggested in the review of
orpc-exception.filter.ts).packages/nest/src/implement.test.ts (3)
20-21: Good test instrumentation for the response handling change.Adding
setStandardResponseSpyalongside the existingsendStandardResponseSpyallows verifying the new response dispatch mechanism.
140-147: Appropriate integration of exception filter in tests.The test setup correctly registers
ORPCExceptionFilterviaAPP_FILTERprovider, which is necessary for thepongtest that throws errors.This ensures consistent error handling behavior across test cases.
427-430: Tests for all StandardBody types are comprehensive and complete.Verification confirms that
response-types.test.tsexists and provides adequate coverage for all requested body types: undefined (7 tests), Blob/File (9 tests), FormData (4 tests), URLSearchParams (6 tests), and async iterators/event streaming (11 tests). ThesetStandardResponseSpyreferences inimplement.test.tsare properly validated against these integration tests.
| export function getStandardHttpBodyAndHeaders( | ||
| body: StandardBody, | ||
| headers: StandardHeaders, | ||
| options: ToNodeHttpBodyOptions = {}, | ||
| ): { body: Readable | undefined | string, headers: StandardHeaders } { | ||
| const newHeaders: StandardHeaders = { ...omit(headers, ['content-disposition', 'content-type']) } | ||
|
|
||
| if (body === undefined) { | ||
| return { body: undefined, headers: newHeaders } | ||
| } | ||
|
|
||
| if (body instanceof Blob) { | ||
| newHeaders['content-type'] = body.type | ||
| newHeaders['content-length'] = body.size.toString() | ||
| const currentContentDisposition = flattenHeader(headers['content-disposition']) | ||
| newHeaders['content-disposition'] = currentContentDisposition ?? generateContentDisposition(body instanceof File ? body.name : 'blob') | ||
|
|
||
| return { body: Readable.fromWeb(body.stream()), headers: newHeaders } | ||
| } | ||
|
|
||
| if (body instanceof FormData) { | ||
| const response = new Response(body) | ||
| newHeaders['content-type'] = response.headers.get('content-type')! | ||
| // The FormData type inferred is from React and not NodeJS, so we need to cast it | ||
| return { body: Readable.fromWeb(response.body as any), headers: newHeaders } | ||
| } | ||
|
|
||
| if (body instanceof URLSearchParams) { | ||
| newHeaders['content-type'] = 'application/x-www-form-urlencoded' | ||
|
|
||
| return { body: body.toString(), headers: newHeaders } | ||
| } | ||
|
|
||
| if (isAsyncIteratorObject(body)) { | ||
| newHeaders['content-type'] = 'text/event-stream' | ||
|
|
||
| return { body: toEventStream(body, options), headers: newHeaders } | ||
| } | ||
|
|
||
| newHeaders['content-type'] = 'application/json' | ||
| // It seems like Nest/Node, in case of a string body, remove or alter the string if | ||
| // content type json is not set. | ||
| // We also need to "double" stringify it, else the string will be encoded as an Array | ||
| // This match the behavior of #toNodeHttpBody | ||
| if (typeof body === 'string') { | ||
| return { body: stringifyJSON(body), headers: newHeaders } | ||
| } | ||
|
|
||
| return { body: body as unknown as string, headers: newHeaders } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Significant code duplication with toNodeHttpBody from standard-server-node.
The logic in getStandardHttpBodyAndHeaders closely mirrors toNodeHttpBody from packages/standard-server-node/src/body.ts (lines 60-104). This creates maintenance burden and potential for divergence.
Consider refactoring to reuse the existing toNodeHttpBody function rather than reimplementing the logic. Based on past review comments, you could:
- Extract the common logic into a shared helper that both functions can use
- Add an option to
toNodeHttpBodyto return headers separately instead of mutating them - Call
toNodeHttpBodyand then process the result
Example approach:
export function getStandardHttpBodyAndHeaders(
body: StandardBody,
headers: StandardHeaders,
options: ToNodeHttpBodyOptions = {},
): { body: Readable | undefined | string, headers: StandardHeaders } {
// Clone headers to avoid mutation
const newHeaders: StandardHeaders = { ...headers }
// Reuse existing logic
const processedBody = toNodeHttpBody(body, newHeaders, options)
return { body: processedBody, headers: newHeaders }
}This would eliminate the duplication while maintaining the desired behavior.
| // We also need to "double" stringify it, else the string will be encoded as an Array | ||
| // This match the behavior of #toNodeHttpBody | ||
| if (typeof body === 'string') { | ||
| return { body: stringifyJSON(body), headers: newHeaders } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another inconsistent behavior 🤕
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, I don't think so, that was the previous behaviour. I tested it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
packages/nest/tests/nest-features.test.ts(1 hunks)packages/nest/tests/response-types.test.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/nest/tests/nest-features.test.ts (3)
packages/contract/src/builder.ts (1)
oc(189-198)packages/nest/src/implement.ts (1)
Injectable(101-178)packages/nest/src/filters/orpc-exception.filter.ts (1)
Catch(44-64)
packages/nest/tests/response-types.test.ts (2)
packages/contract/src/builder.ts (1)
oc(189-198)packages/nest/tests/nest-features.test.ts (1)
data(252-261)
🔇 Additional comments (9)
packages/nest/tests/response-types.test.ts (3)
14-112: Excellent documentation and comprehensive contract coverage.The test contracts cover all ORPC standard body types as documented. The inline documentation provides clear context for the test purpose and references.
118-250: Well-structured controller implementations.Each handler correctly implements its contract using the appropriate response type. The implementations demonstrate proper usage of ORPC features like
withEventMetafor SSE streams and detailed output structures.
463-542: Proper SSE implementation with Last-Event-ID.The event metadata tests correctly implement SSE reconnection semantics using the
Last-Event-IDheader. The use of GET requests without bodies is appropriate for SSE streams.packages/nest/tests/nest-features.test.ts (6)
1-85: Good test setup with appropriate middleware imports.The test file properly imports both Express and Fastify compression middleware for testing compression behavior across adapters. The contracts comprehensively cover different NestJS features (guards, pipes, filters, interceptors).
88-262: Well-structured controllers demonstrating NestJS features.Each controller correctly demonstrates its respective NestJS feature:
- Guards properly modify the request object (setting
request.user)- Error controller throws HttpException for filter testing
- Compression controller returns sufficient data (>1KB) to trigger compression
The guard implementation at lines 175-190 properly simulates JWT-style authentication by adding user data to the request, which is then accessible in the handler.
264-296: Global interceptor and filter correctly test PR objectives.The implementations demonstrate the PR's goal of allowing NestJS middleware to modify ORPC responses:
- GlobalLoggingInterceptor modifies both response body and headers
- GlobalHttpExceptionFilter bypasses ORPC's exception handling to provide custom error formatting
These patterns align with the PR's objective to restore standard NestJS feature compatibility.
298-398: Proper module configuration with NestJS providers.All test modules correctly configure their respective features using NestJS's provider system (APP_PIPE, APP_FILTER, APP_INTERCEPTOR). The compression middleware is properly applied using
compression()for Express (line 396).
483-514: Excellent documentation of pipe behavior with ORPC.The detailed comment (lines 483-497) provides valuable context about why NestJS global pipes don't transform ORPC handler inputs and offers clear alternatives. This prevents user confusion and documents expected behavior. The test correctly validates that pipes coexist with ORPC without causing errors.
545-608: Proper verification of exception filter precedence.The test correctly verifies that when a global NestJS exception filter handles an error, ORPC's exception handling is bypassed (line 574). This demonstrates that the PR successfully allows standard NestJS filters to intercept errors before ORPC's error handling.
The compression test properly validates that middleware can access and compress the ORPC response body, which was a key objective of this PR.
| eventStream: oc.route({ | ||
| path: '/event-stream', | ||
| method: 'GET', | ||
| }).input(z.object({ count: z.number().optional() }).optional()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GET request with body is non-standard and unreliable.
The eventStream contract defines a GET method with input body (lines 89-92), and the test attempts to send a body with GET requests (lines 427-428). While the test includes fallback logic to skip when the response is undefined, this approach is problematic:
- GET requests with bodies violate HTTP semantics and have inconsistent support across servers, proxies, and clients.
- The fallback makes the test non-deterministic—it may pass by skipping rather than validating functionality.
Consider one of these solutions:
Solution 1 (Recommended): Use query parameters for GET
eventStream: oc.route({
path: '/event-stream',
method: 'GET',
}).input(z.object({ count: z.number().optional() }).optional()),Then modify the test to use query parameters:
-await request(app.getHttpServer())
- .get('/event-stream')
- .send({ count: 3 })
+await request(app.getHttpServer())
+ .get('/event-stream?count=3')Solution 2: Change to POST method
eventStream: oc.route({
path: '/event-stream',
- method: 'GET',
+ method: 'POST',
}).input(z.object({ count: z.number().optional() }).optional()),Also applies to: 205-215, 421-461
I'm working on our integration. That should inform some real world issues that remains. |
This PR all the tested features of Nest to integrate better with orpc and it's feature.
For example:
ORPCExceptionFilterto keep the same behaviour.Basically everything that was trying to modify the response after the handler was failing due to the handler sending the response.
The handler now set headers, status code, return the proper body and let the middleware chain resolve and let Nest/Node send the response at the end.
Summary by CodeRabbit
New Features
Documentation
Tests