typescriptdependency-injectionnestjsnestjs-interceptor

Registered Nest.js interceptor not being used for dependency injection


Is there a trick to getting useValue dependency injection working properly for Nest.js interceptors? I have a dynamic module similar to this:

@Module({})
export class SomeFeatureModule {
  static register({
    perRequestParams,
    ...clientOptions
  }: ModuleOptions): DynamicModule {
    const provider = new SomeClientProvider(clientOptions);
    return {
      module: SomeFeatureModule,
      providers: [
        {
          provide: SomeClientProvider,
          useValue: provider,
        },
        {
          provide: SomeInterceptor,
          useValue: new SomeInterceptor(provider, perRequestParams),
        },
      ],
      exports: [SomeClientProvider, SomeInterceptor],
    };
  }
}

...where the SomeInterceptor class is something like this:

@Injectable()
export class SomeInterceptor implements NestInterceptor {
  constructor(
    private readonly someClientProvider: SomeClientProvider,
    private readonly perRequestParams: (
      context: ExecutionContext,
    ) => EvaluationCriteria | Promise<EvaluationCriteria>,
  ) {}

  async intercept(
    execContext: ExecutionContext,
    next: CallHandler<any>,
  ): Promise<Observable<any>> {
    const params = await this.perRequestParams(execContext);
    return this.someClientProvider.injectLocalStorageData(params, () => next.handle());
  }
}

...but then when I try to use the interceptor on my app's controller:

@UseInterceptors(SomeInterceptor)

...I get the error:

Error: Nest can't resolve dependencies of the SomeInterceptor (SomeClientProvider, ?). Please make sure that the argument Function at index [1] is available in the AppModule context.

I am specifically importing SomeFeatureModule.register(...) in my AppModule:

@Module({})
export class AppModule {
  static register(env: Environment): DynamicModule {
    // ...
    return {
      module: AppModule,
      imports: [
        SomeFeatureModule.register({
          ...clientConfig,
          async perRequestParams(ctx) {
            // ...
          },
        }),
      ],
      // ...
    };
  }
}

Why is the dependency injection system trying to resolve the constructor parameters for SomeInterceptor even though I'm already manually providing one?

Note that if I remove @Injectable() I don't get the same startup error, but the interceptor's constructor is called with no arguments, so that is also broken.


Solution

  • I found a workaround. Since it seems that the registered SomeInterceptor is being ignored, instead I declared a new Symbol that the user of the interceptor has to register a value for:

    export const PER_REQUEST_PARAMS = Symbol('PER_REQUEST_PARAMS');
    

    New SomeInterceptor:

    @Injectable()
    export class SomeInterceptor implements NestInterceptor {
      constructor(
        private readonly clientProvider: SomeClientProvider,
        @Inject(PER_REQUEST_PARAMS)
        private readonly perRequestParams: (
          context: ExecutionContext,
        ) => EvaluationCriteria | Promise<EvaluationCriteria>,
      ) {}
    
      // ...
    }
    

    ...and now AppModule has to do this:

    @Module({})
    export class AppModule {
      static register(env: Environment): DynamicModule {
        // ...
        return {
          module: AppModule,
          imports: [
            SomeFeatureModule.register(clientConfig),
            // ...
          ],
          providers: [
            // ...
            {
              provide: PER_REQUEST_PARAMS,
              useValue: (ctx: ExecutionContext) => {
                // ...
              }
            }
          ]
          // ...
        };
      }
    }
    

    Strangely, providing PER_REQUEST_PARAMS from within SomeFeatureModule doesn't work; it seems that I have to do it in AppModule or it doesn't resolve. Hopefully this is a bug because it's not very intuitive.

    Update:

    I found that if I do register a value for PER_REQUEST_PARAMS within SomeFeatureModule it does work, but I also have to make sure to export PER_REQUEST_PARAMS, otherwise it isn't seen when instantiating an instance of SomeInterceptor.