@@ -519,6 +519,104 @@ describe('nestjsChannelIntegration: @Catch (exception filter)', () => {
519519 new HttpExceptionFilter ( ) . catch ( 'boom' , undefined ) ;
520520 expect ( spanInside ) . toBeUndefined ( ) ;
521521 } ) ;
522+
523+ // A class can be decorated with both `@Injectable` and `@Catch` (an exception
524+ // filter that uses DI). Which channel fires first depends on decorator
525+ // stacking order (decorators apply inner-first): `@Catch` over `@Injectable`
526+ // fires @Injectable first; `@Injectable` over `@Catch` fires @Catch first.
527+ // Because the two passes use separate patched-flags, both must wrap their own
528+ // methods regardless of which channel fires first.
529+ function fireInjectable ( target : object ) : void {
530+ tracingChannel < { arguments : unknown [ ] } > ( CHANNELS . NESTJS_INJECTABLE ) . traceSync ( ( ) => undefined , {
531+ arguments : [ target ] ,
532+ } ) ;
533+ }
534+
535+ it ( 'still wraps `catch` when the @Injectable channel fired first (dual @Injectable @Catch filter)' , ( ) => {
536+ installTestAsyncContextStrategy ( ) ;
537+ initTestClient ( ) ;
538+ nestjsChannelIntegration ( ) . setupOnce ! ( ) ;
539+
540+ let spanInside : ReturnType < typeof getActiveSpan > ;
541+ class HttpExceptionFilter {
542+ public catch ( exception : unknown , _host : unknown ) : string {
543+ spanInside = getActiveSpan ( ) ;
544+ return `handled:${ String ( exception ) } ` ;
545+ }
546+ }
547+ fireInjectable ( HttpExceptionFilter ) ;
548+ applyCatch ( HttpExceptionFilter ) ;
549+
550+ const ret = new HttpExceptionFilter ( ) . catch ( 'boom' , { switchToHttp : ( ) => ( { } ) } ) ;
551+ expect ( ret ) . toBe ( 'handled:boom' ) ;
552+
553+ const json = spanToJSON ( spanInside ! ) ;
554+ expect ( json . description ) . toBe ( 'HttpExceptionFilter' ) ;
555+ expect ( json . op ) . toBe ( 'middleware.nestjs' ) ;
556+ expect ( json . origin ) . toBe ( 'auto.middleware.orchestrion.nestjs.exception_filter' ) ;
557+ } ) ;
558+
559+ it ( 'still wraps `catch` when the @Catch channel fired first (dual @Injectable @Catch filter)' , ( ) => {
560+ installTestAsyncContextStrategy ( ) ;
561+ initTestClient ( ) ;
562+ nestjsChannelIntegration ( ) . setupOnce ! ( ) ;
563+
564+ let spanInside : ReturnType < typeof getActiveSpan > ;
565+ class HttpExceptionFilter {
566+ public catch ( exception : unknown , _host : unknown ) : string {
567+ spanInside = getActiveSpan ( ) ;
568+ return `handled:${ String ( exception ) } ` ;
569+ }
570+ }
571+ applyCatch ( HttpExceptionFilter ) ;
572+ fireInjectable ( HttpExceptionFilter ) ;
573+
574+ const ret = new HttpExceptionFilter ( ) . catch ( 'boom' , { switchToHttp : ( ) => ( { } ) } ) ;
575+ expect ( ret ) . toBe ( 'handled:boom' ) ;
576+
577+ const json = spanToJSON ( spanInside ! ) ;
578+ expect ( json . description ) . toBe ( 'HttpExceptionFilter' ) ;
579+ expect ( json . op ) . toBe ( 'middleware.nestjs' ) ;
580+ expect ( json . origin ) . toBe ( 'auto.middleware.orchestrion.nestjs.exception_filter' ) ;
581+ } ) ;
582+
583+ // A (contrived) class that is BOTH a guard (`canActivate`) and an exception
584+ // filter (`catch`) proves the two passes are independent: neither ordering may
585+ // let one pass's patched-flag block the other. Both spans must appear either way.
586+ for ( const order of [ 'injectable-first' , 'catch-first' ] as const ) {
587+ it ( `wraps BOTH canActivate and catch when the ${ order } channel fired first` , ( ) => {
588+ installTestAsyncContextStrategy ( ) ;
589+ initTestClient ( ) ;
590+ nestjsChannelIntegration ( ) . setupOnce ! ( ) ;
591+
592+ let guardSpan : ReturnType < typeof getActiveSpan > ;
593+ let filterSpan : ReturnType < typeof getActiveSpan > ;
594+ class GuardAndFilter {
595+ public canActivate ( _ctx : unknown ) : boolean {
596+ guardSpan = getActiveSpan ( ) ;
597+ return true ;
598+ }
599+ public catch ( exception : unknown , _host : unknown ) : string {
600+ filterSpan = getActiveSpan ( ) ;
601+ return `handled:${ String ( exception ) } ` ;
602+ }
603+ }
604+
605+ if ( order === 'injectable-first' ) {
606+ fireInjectable ( GuardAndFilter ) ;
607+ applyCatch ( GuardAndFilter ) ;
608+ } else {
609+ applyCatch ( GuardAndFilter ) ;
610+ fireInjectable ( GuardAndFilter ) ;
611+ }
612+
613+ expect ( new GuardAndFilter ( ) . canActivate ( { ctx : true } ) ) . toBe ( true ) ;
614+ expect ( new GuardAndFilter ( ) . catch ( 'boom' , { switchToHttp : ( ) => ( { } ) } ) ) . toBe ( 'handled:boom' ) ;
615+
616+ expect ( spanToJSON ( guardSpan ! ) . origin ) . toBe ( 'auto.middleware.orchestrion.nestjs.guard' ) ;
617+ expect ( spanToJSON ( filterSpan ! ) . origin ) . toBe ( 'auto.middleware.orchestrion.nestjs.exception_filter' ) ;
618+ } ) ;
619+ }
522620} ) ;
523621
524622describe ( 'nestjsChannelIntegration: schedule / event / bullmq' , ( ) => {
0 commit comments