11import type { ArgumentsHost , CallHandler , CanActivate , ExceptionFilter , ExecutionContext , INestApplication , MiddlewareConsumer , NestInterceptor , NestMiddleware , PipeTransform } from '@nestjs/common'
2+ import type { NestFastifyApplication } from '@nestjs/platform-fastify'
23import type { Observable } from 'rxjs'
3- import { Catch , Controller , ForbiddenException , HttpException , Injectable , Module , SetMetadata , UseGuards , UseInterceptors } from '@nestjs/common'
4+ import fastifyCompress from '@fastify/compress'
5+ import { Catch , Controller , ForbiddenException , HttpException , Injectable , Module , Req , SetMetadata , UseGuards , UseInterceptors } from '@nestjs/common'
46import { APP_FILTER , APP_INTERCEPTOR , APP_PIPE } from '@nestjs/core'
57import { ExpressAdapter } from '@nestjs/platform-express'
68import { FastifyAdapter } from '@nestjs/platform-fastify'
79import { Test } from '@nestjs/testing'
810import { oc } from '@orpc/contract'
911import { implement } from '@orpc/server'
1012import * as StandardServerNode from '@orpc/standard-server-node'
13+ // eslint-disable-next-line no-restricted-imports -- needed for testing compression middleware integration
14+ import compression from 'compression'
1115import { map } from 'rxjs'
1216import request from 'supertest'
1317import { afterEach , describe , expect , it , vi } from 'vitest'
1418import { z } from 'zod'
1519
16- import { Implement , ORPCExceptionFilter , ORPCModule } from '.'
20+ import { Implement , ORPCModule } from '.'
1721
1822// 1. oRPC Contract
1923const testContract = {
@@ -71,7 +75,16 @@ const testGuardContract = {
7175 . output ( z . object ( { message : z . string ( ) , user : z . string ( ) } ) ) ,
7276}
7377
74- // 2. A real controller for the 'raw' output test
78+ // Contract for testing compression middleware
79+ const testCompressionContract = {
80+ data : oc . route ( {
81+ path : '/large-data' ,
82+ method : 'GET' ,
83+ } )
84+ . output ( z . object ( { data : z . string ( ) , size : z . number ( ) } ) ) ,
85+ }
86+
87+ // 2. A controller for the 'raw' output test
7588@Controller ( )
7689class TestRawController {
7790 @Implement ( testContract . hello )
@@ -98,7 +111,7 @@ class TestDetailedController {
98111 }
99112}
100113
101- // 4. Interceptor that modifies the response body (must be declared before controllers that use it)
114+ // 4. Interceptor that modifies the response body
102115@Injectable ( )
103116class ResponseTransformInterceptor implements NestInterceptor {
104117 intercept ( context : ExecutionContext , next : CallHandler ) : Observable < any > {
@@ -113,8 +126,7 @@ class ResponseTransformInterceptor implements NestInterceptor {
113126 )
114127 }
115128}
116-
117- // 5. Controller with interceptor to test response transformation
129+ // 4. Controller with interceptor to test response transformation
118130@Controller ( )
119131class TestInterceptorController {
120132 @Implement ( testContract . hello )
@@ -193,20 +205,18 @@ class AuthenticatedGuard implements CanActivate {
193205}
194206
195207// 11. Controller for testing guards (method-level)
196- // IMPORTANT: The @Implement interceptor passes the NestJS request/response
197- // as part of the oRPC context, allowing handlers to access guard modifications
198- // like properties set on the original request (standard practice).
199- // This solves the limitation where oRPC creates its own standardized request object.
208+ // Also try accessing request modifications made by guards
200209@Controller ( )
201210class TestGuardController {
202211 @UseGuards ( ApiKeyGuard , AuthenticatedGuard )
203212 @RequireRole ( 'admin' )
204213 @Implement ( testGuardContract . protected )
205- protected ( ) {
206- return implement ( testGuardContract . protected ) . handler ( async ( { input, context } ) => {
207- const req = ( context as any ) . request
214+ protected (
215+ @Req ( )
216+ req : any ,
217+ ) {
218+ return implement ( testGuardContract . protected ) . handler ( async ( ) => {
208219 const user = req ?. user
209-
210220 return {
211221 message : 'Access granted' ,
212222 user : user ?. name || 'Unknown' ,
@@ -235,6 +245,22 @@ class UpperCasePipe implements PipeTransform {
235245 }
236246}
237247
248+ // 13. Controller for testing compression middleware
249+ @Controller ( )
250+ class TestCompressionController {
251+ @Implement ( testCompressionContract . data )
252+ data ( ) {
253+ return implement ( testCompressionContract . data ) . handler ( async ( ) => {
254+ // Return a response that can be compressed (1kb minimum)
255+ const largeData = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' . repeat ( 50 )
256+ return {
257+ data : largeData ,
258+ size : largeData . length ,
259+ }
260+ } )
261+ }
262+ }
263+
238264// 9. Global interceptor that modifies the response
239265@Injectable ( )
240266class GlobalLoggingInterceptor implements NestInterceptor {
@@ -249,7 +275,7 @@ class GlobalLoggingInterceptor implements NestInterceptor {
249275 }
250276}
251277
252- // 9 . Global filter that catches HTTP exceptions
278+ // 10 . Global filter that catches HTTP exceptions
253279@Catch ( HttpException )
254280class GlobalHttpExceptionFilter implements ExceptionFilter {
255281 catch ( exception : HttpException , host : ArgumentsHost ) {
@@ -269,23 +295,18 @@ class GlobalHttpExceptionFilter implements ExceptionFilter {
269295 }
270296}
271297
272- // 10 . Custom Middleware
298+ // 11 . Custom Middleware
273299class CustomHeaderMiddleware implements NestMiddleware {
274300 use ( req : any , res : any , next : ( error ?: any ) => void ) {
275301 res . setHeader ( 'X-Custom-Middleware' , 'hello' )
276302 next ( )
277303 }
278304}
279305
280- // 10. Test Modules for each controller
306+ // Test Modules for each controller
281307@Module ( {
282308 controllers : [ TestRawController ] ,
283- providers : [
284- {
285- provide : APP_FILTER ,
286- useClass : ORPCExceptionFilter ,
287- } ,
288- ] ,
309+ providers : [ ] ,
289310} )
290311class TestRawModule {
291312 configure ( consumer : MiddlewareConsumer ) {
@@ -295,12 +316,7 @@ class TestRawModule {
295316
296317@Module ( {
297318 controllers : [ TestDetailedController ] ,
298- providers : [
299- {
300- provide : APP_FILTER ,
301- useClass : ORPCExceptionFilter ,
302- } ,
303- ] ,
319+ providers : [ ] ,
304320} )
305321class TestDetailedModule {
306322 configure ( consumer : MiddlewareConsumer ) {
@@ -312,10 +328,6 @@ class TestDetailedModule {
312328 controllers : [ TestInterceptorController ] ,
313329 providers : [
314330 ResponseTransformInterceptor ,
315- {
316- provide : APP_FILTER ,
317- useClass : ORPCExceptionFilter ,
318- } ,
319331 ] ,
320332} )
321333class TestInterceptorModule {
@@ -331,10 +343,6 @@ class TestInterceptorModule {
331343 provide : APP_PIPE ,
332344 useClass : UpperCasePipe ,
333345 } ,
334- {
335- provide : APP_FILTER ,
336- useClass : ORPCExceptionFilter ,
337- } ,
338346 ] ,
339347} )
340348class TestPipeModule {
@@ -350,23 +358,13 @@ class TestPipeModule {
350358 provide : APP_FILTER ,
351359 useClass : GlobalHttpExceptionFilter ,
352360 } ,
353- // this will not run because GlobalHttpExceptionFilter sends the response first
354- {
355- provide : APP_FILTER ,
356- useClass : ORPCExceptionFilter ,
357- } ,
358361 ] ,
359362} )
360363class TestErrorModule { }
361364
362365@Module ( {
363366 controllers : [ TestGuardController ] ,
364- providers : [
365- {
366- provide : APP_FILTER ,
367- useClass : ORPCExceptionFilter ,
368- } ,
369- ] ,
367+ providers : [ ] ,
370368} )
371369class TestGuardModule {
372370 configure ( consumer : MiddlewareConsumer ) {
@@ -381,10 +379,6 @@ class TestGuardModule {
381379 provide : APP_INTERCEPTOR ,
382380 useClass : GlobalLoggingInterceptor ,
383381 } ,
384- {
385- provide : APP_FILTER ,
386- useClass : ORPCExceptionFilter ,
387- } ,
388382 ] ,
389383} )
390384class TestGlobalInterceptorModule {
@@ -393,6 +387,16 @@ class TestGlobalInterceptorModule {
393387 }
394388}
395389
390+ @Module ( {
391+ controllers : [ TestCompressionController ] ,
392+ } )
393+ class TestCompressionModule {
394+ configure ( consumer : MiddlewareConsumer ) {
395+ // Use the actual compression middleware for Express
396+ consumer . apply ( compression ( ) ) . forRoutes ( '*' )
397+ }
398+ }
399+
396400const sendStandardResponseSpy = vi . spyOn ( StandardServerNode , 'sendStandardResponse' )
397401
398402describe ( 'oRPC Nest Middleware Integration' , ( ) => {
@@ -410,6 +414,12 @@ describe('oRPC Nest Middleware Integration', () => {
410414
411415 app = moduleFixture . createNestApplication ( adapter ( ) )
412416 app . enableCors ( )
417+
418+ // Register compression for Fastify
419+ if ( adapterName === 'Fastify' ) {
420+ await ( app as NestFastifyApplication ) . register ( fastifyCompress )
421+ }
422+
413423 await app . init ( )
414424 if ( adapterName === 'Fastify' ) {
415425 await ( app as any ) . getHttpAdapter ( ) . getInstance ( ) . ready ( )
@@ -576,6 +586,25 @@ describe('oRPC Nest Middleware Integration', () => {
576586 expect ( response . body . globalInterceptor ) . toBe ( true )
577587 } )
578588 } )
589+
590+ it ( 'should work with compression middleware that accesses oRPC response body' , async ( ) => {
591+ await createApp ( TestCompressionModule , { } )
592+
593+ // Make a request with Accept-Encoding header to enable compression
594+ const response = await request ( app . getHttpServer ( ) )
595+ . get ( '/large-data' )
596+ . set ( 'Accept-Encoding' , 'gzip, deflate' )
597+ . expect ( 200 )
598+
599+ // Verify compression was applied (check for content-encoding header)
600+ // Note: compression middleware only compresses responses above a certain threshold (default 1kb)
601+ expect ( [ 'gzip' , 'deflate' ] ) . toContain ( response . headers [ 'content-encoding' ] )
602+
603+ // Verify that the oRPC handler response is correctly returned (supertest auto-decompresses)
604+ expect ( response . body ) . toHaveProperty ( 'data' )
605+ expect ( response . body ) . toHaveProperty ( 'size' )
606+ expect ( response . body . size ) . toBeGreaterThan ( 0 )
607+ } )
579608 } )
580609 }
581610
0 commit comments