• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nestjs / nest / 4afa0176-b936-482c-ad88-b635a9db93b4

14 Jul 2025 11:37AM UTC coverage: 88.866% (-0.02%) from 88.886%
4afa0176-b936-482c-ad88-b635a9db93b4

Pull #15386

circleci

kamilmysliwiec
style: address linter warnings
Pull Request #15386: feat: enhance introspection capabilities

2714 of 3431 branches covered (79.1%)

101 of 118 new or added lines in 15 files covered. (85.59%)

12 existing lines in 1 file now uncovered.

7239 of 8146 relevant lines covered (88.87%)

16.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.41
/packages/core/router/router-explorer.ts
1
import { HttpServer } from '@nestjs/common';
2
import { PATH_METADATA } from '@nestjs/common/constants';
1✔
3
import { RequestMethod, VersioningType } from '@nestjs/common/enums';
1✔
4
import { InternalServerErrorException } from '@nestjs/common/exceptions';
1✔
5
import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface';
6
import { Type } from '@nestjs/common/interfaces/type.interface';
7
import { VersionValue } from '@nestjs/common/interfaces/version-options.interface';
8
import { Logger } from '@nestjs/common/services/logger.service';
1✔
9
import {
1✔
10
  addLeadingSlash,
11
  isUndefined,
12
} from '@nestjs/common/utils/shared.utils';
13
import { pathToRegexp } from 'path-to-regexp';
1✔
14
import { ApplicationConfig } from '../application-config';
15
import { UnknownRequestMappingException } from '../errors/exceptions/unknown-request-mapping.exception';
1✔
16
import { GuardsConsumer, GuardsContextCreator } from '../guards';
1✔
17
import { ContextIdFactory } from '../helpers/context-id-factory';
1✔
18
import { ExecutionContextHost } from '../helpers/execution-context-host';
1✔
19
import {
1✔
20
  ROUTE_MAPPED_MESSAGE,
21
  VERSIONED_ROUTE_MAPPED_MESSAGE,
22
} from '../helpers/messages';
23
import { RouterMethodFactory } from '../helpers/router-method-factory';
1✔
24
import { STATIC_CONTEXT } from '../injector/constants';
1✔
25
import { NestContainer } from '../injector/container';
26
import { Injector } from '../injector/injector';
27
import { ContextId, InstanceWrapper } from '../injector/instance-wrapper';
28
import { Module } from '../injector/module';
29
import { GraphInspector } from '../inspector/graph-inspector';
30
import {
31
  Entrypoint,
32
  HttpEntrypointMetadata,
33
} from '../inspector/interfaces/entrypoint.interface';
34
import {
1✔
35
  InterceptorsConsumer,
36
  InterceptorsContextCreator,
37
} from '../interceptors';
38
import { MetadataScanner } from '../metadata-scanner';
39
import { PipesConsumer, PipesContextCreator } from '../pipes';
1✔
40
import { ExceptionsFilter } from './interfaces/exceptions-filter.interface';
41
import { RoutePathMetadata } from './interfaces/route-path-metadata.interface';
42
import { PathsExplorer } from './paths-explorer';
1✔
43
import { REQUEST_CONTEXT_ID } from './request/request-constants';
1✔
44
import { RouteParamsFactory } from './route-params-factory';
1✔
45
import { RoutePathFactory } from './route-path-factory';
46
import { RouterExecutionContext } from './router-execution-context';
1✔
47
import { RouterProxy, RouterProxyCallback } from './router-proxy';
48

49
export interface RouteDefinition {
50
  path: string[];
51
  requestMethod: RequestMethod;
52
  targetCallback: RouterProxyCallback;
53
  methodName: string;
54
  version?: VersionValue;
55
}
56

57
export class RouterExplorer {
1✔
58
  private readonly executionContextCreator: RouterExecutionContext;
59
  private readonly pathsExplorer: PathsExplorer;
60
  private readonly routerMethodFactory = new RouterMethodFactory();
28✔
61
  private readonly logger = new Logger(RouterExplorer.name, {
28✔
62
    timestamp: true,
63
  });
64
  private readonly exceptionFiltersCache = new WeakMap();
28✔
65

66
  constructor(
67
    metadataScanner: MetadataScanner,
68
    private readonly container: NestContainer,
28✔
69
    private readonly injector: Injector,
28✔
70
    private readonly routerProxy: RouterProxy,
28✔
71
    private readonly exceptionsFilter: ExceptionsFilter,
28✔
72
    config: ApplicationConfig,
73
    private readonly routePathFactory: RoutePathFactory,
28✔
74
    private readonly graphInspector: GraphInspector,
28✔
75
  ) {
76
    this.pathsExplorer = new PathsExplorer(metadataScanner);
28✔
77

78
    const routeParamsFactory = new RouteParamsFactory();
28✔
79
    const pipesContextCreator = new PipesContextCreator(container, config);
28✔
80
    const pipesConsumer = new PipesConsumer();
28✔
81
    const guardsContextCreator = new GuardsContextCreator(container, config);
28✔
82
    const guardsConsumer = new GuardsConsumer();
28✔
83
    const interceptorsContextCreator = new InterceptorsContextCreator(
28✔
84
      container,
85
      config,
86
    );
87
    const interceptorsConsumer = new InterceptorsConsumer();
28✔
88

89
    this.executionContextCreator = new RouterExecutionContext(
28✔
90
      routeParamsFactory,
91
      pipesContextCreator,
92
      pipesConsumer,
93
      guardsContextCreator,
94
      guardsConsumer,
95
      interceptorsContextCreator,
96
      interceptorsConsumer,
97
      container.getHttpAdapterRef(),
98
    );
99
  }
100

101
  public explore<T extends HttpServer = any>(
102
    instanceWrapper: InstanceWrapper,
103
    moduleKey: string,
104
    httpAdapterRef: T,
105
    host: string | RegExp | Array<string | RegExp>,
106
    routePathMetadata: RoutePathMetadata,
107
  ) {
108
    const { instance } = instanceWrapper;
3✔
109
    const routerPaths = this.pathsExplorer.scanForPaths(instance);
3✔
110
    this.applyPathsToRouterProxy(
3✔
111
      httpAdapterRef,
112
      routerPaths,
113
      instanceWrapper,
114
      moduleKey,
115
      routePathMetadata,
116
      host,
117
    );
118
  }
119

120
  public extractRouterPath(metatype: Type<Controller>): string[] {
121
    const path = Reflect.getMetadata(PATH_METADATA, metatype);
3✔
122

123
    if (isUndefined(path)) {
3✔
124
      throw new UnknownRequestMappingException(metatype);
1✔
125
    }
126
    if (Array.isArray(path)) {
2✔
127
      return path.map(p => addLeadingSlash(p));
2✔
128
    }
129
    return [addLeadingSlash(path)];
1✔
130
  }
131

132
  public applyPathsToRouterProxy<T extends HttpServer>(
133
    router: T,
134
    routeDefinitions: RouteDefinition[],
135
    instanceWrapper: InstanceWrapper,
136
    moduleKey: string,
137
    routePathMetadata: RoutePathMetadata,
138
    host: string | RegExp | Array<string | RegExp>,
139
  ) {
140
    (routeDefinitions || []).forEach(routeDefinition => {
5!
141
      const { version: methodVersion } = routeDefinition;
10✔
142
      routePathMetadata.methodVersion = methodVersion;
10✔
143

144
      this.applyCallbackToRouter(
10✔
145
        router,
146
        routeDefinition,
147
        instanceWrapper,
148
        moduleKey,
149
        routePathMetadata,
150
        host,
151
      );
152
    });
153
  }
154

155
  private applyCallbackToRouter<T extends HttpServer>(
156
    router: T,
157
    routeDefinition: RouteDefinition,
158
    instanceWrapper: InstanceWrapper,
159
    moduleKey: string,
160
    routePathMetadata: RoutePathMetadata,
161
    host: string | RegExp | Array<string | RegExp>,
162
  ) {
163
    const {
164
      path: paths,
165
      requestMethod,
166
      targetCallback,
167
      methodName,
168
    } = routeDefinition;
4✔
169

170
    const { instance } = instanceWrapper;
4✔
171
    const routerMethodRef = this.routerMethodFactory
4✔
172
      .get(router, requestMethod)
173
      .bind(router);
174

175
    const isRequestScoped = !instanceWrapper.isDependencyTreeStatic();
4✔
176
    const proxy = isRequestScoped
4✔
177
      ? this.createRequestScopedHandler(
4!
178
          instanceWrapper,
179
          requestMethod,
180
          this.container.getModuleByKey(moduleKey)!,
181
          moduleKey,
182
          methodName,
183
        )
184
      : this.createCallbackProxy(
185
          instance,
186
          targetCallback,
187
          methodName,
188
          moduleKey,
189
          requestMethod,
190
        );
191

192
    const isVersioned =
193
      (routePathMetadata.methodVersion ||
4✔
194
        routePathMetadata.controllerVersion) &&
195
      routePathMetadata.versioningOptions;
196
    let routeHandler = this.applyHostFilter(host, proxy);
4✔
197

198
    paths.forEach(path => {
4✔
199
      if (
4!
200
        isVersioned &&
5✔
201
        routePathMetadata.versioningOptions!.type !== VersioningType.URI
202
      ) {
203
        // All versioning (except for URI Versioning) is done via the "Version Filter"
204
        routeHandler = this.applyVersionFilter(
×
205
          router,
206
          routePathMetadata,
207
          routeHandler,
208
        );
209
      }
210

211
      routePathMetadata.methodPath = path;
4✔
212
      const pathsToRegister = this.routePathFactory.create(
4✔
213
        routePathMetadata,
214
        requestMethod,
215
      );
216
      pathsToRegister.forEach(path => {
4✔
217
        const entrypointDefinition: Entrypoint<HttpEntrypointMetadata> = {
4✔
218
          type: 'http-endpoint',
219
          methodName,
220
          className: instanceWrapper.name,
221
          classNodeId: instanceWrapper.id,
222
          metadata: {
223
            key: path,
224
            path,
225
            requestMethod: RequestMethod[
226
              requestMethod
227
            ] as keyof typeof RequestMethod,
228
            methodVersion: routePathMetadata.methodVersion,
229
            controllerVersion: routePathMetadata.controllerVersion,
230
          },
231
        };
232

233
        this.copyMetadataToCallback(targetCallback, routeHandler);
4✔
234
        const normalizedPath = router.normalizePath
4✔
235
          ? router.normalizePath(path)
4!
236
          : path;
237

238
        const httpAdapter = this.container.getHttpAdapterRef();
4✔
239
        const onRouteTriggered = httpAdapter.getOnRouteTriggered?.();
4✔
240
        if (onRouteTriggered) {
4!
NEW
241
          routerMethodRef(normalizedPath, (...args: unknown[]) => {
×
NEW
242
            onRouteTriggered(requestMethod, path);
×
NEW
243
            return routeHandler(...args);
×
244
          });
245
        } else {
246
          routerMethodRef(normalizedPath, routeHandler);
4✔
247
        }
248

249
        this.graphInspector.insertEntrypointDefinition<HttpEntrypointMetadata>(
4✔
250
          entrypointDefinition,
251
          instanceWrapper.id,
252
        );
253
      });
254

255
      const pathsToLog = this.routePathFactory.create(
4✔
256
        {
257
          ...routePathMetadata,
258
          versioningOptions: undefined,
259
        },
260
        requestMethod,
261
      );
262
      pathsToLog.forEach(path => {
4✔
263
        if (isVersioned) {
4✔
264
          const version = this.routePathFactory.getVersion(routePathMetadata);
1✔
265
          this.logger.log(
1✔
266
            VERSIONED_ROUTE_MAPPED_MESSAGE(path, requestMethod, version!),
267
          );
268
        } else {
269
          this.logger.log(ROUTE_MAPPED_MESSAGE(path, requestMethod));
3✔
270
        }
271
      });
272
    });
273
  }
274

275
  private applyHostFilter(
276
    host: string | RegExp | Array<string | RegExp>,
277
    handler: Function,
278
  ) {
279
    if (!host) {
4✔
280
      return handler;
3✔
281
    }
282

283
    const httpAdapterRef = this.container.getHttpAdapterRef();
1✔
284
    const hosts = Array.isArray(host) ? host : [host];
1!
285
    const hostRegExps = hosts.map((host: string | RegExp) => {
1✔
286
      if (typeof host === 'string') {
1!
287
        try {
1✔
288
          return pathToRegexp(host);
1✔
289
        } catch (e) {
290
          if (e instanceof TypeError) {
×
291
            this.logger.error(
×
292
              `Unsupported host "${host}" syntax. In past releases, ?, *, and + were used to denote optional or repeating path parameters. The latest version of "path-to-regexp" now requires the use of named parameters. For example, instead of using a route like /users/* to capture all routes starting with "/users", you should use /users/*path. Please see the migration guide for more information.`,
293
            );
294
          }
295
          throw e;
×
296
        }
297
      }
298
      return { regexp: host, keys: [] };
×
299
    });
300

301
    const unsupportedFilteringErrorMessage = Array.isArray(host)
1✔
302
      ? `HTTP adapter does not support filtering on hosts: ["${host.join(
1!
303
          '", "',
304
        )}"]`
305
      : `HTTP adapter does not support filtering on host: "${host}"`;
306

307
    return <TRequest extends Record<string, any> = any, TResponse = any>(
1✔
308
      req: TRequest,
309
      res: TResponse,
310
      next: () => void,
311
    ) => {
312
      (req as Record<string, any>).hosts = {};
×
313
      const hostname = httpAdapterRef.getRequestHostname(req) || '';
×
314

315
      for (const exp of hostRegExps) {
×
316
        const match = hostname.match(exp.regexp);
×
317
        if (match) {
×
318
          if (exp.keys.length > 0) {
×
319
            exp.keys.forEach((key, i) => (req.hosts[key.name] = match[i + 1]));
×
320
          } else if (exp.regexp && match.groups) {
×
321
            for (const groupName in match.groups) {
×
322
              req.hosts[groupName] = match.groups[groupName];
×
323
            }
324
          }
325
          return handler(req, res, next);
×
326
        }
327
      }
328
      if (!next) {
×
329
        throw new InternalServerErrorException(
×
330
          unsupportedFilteringErrorMessage,
331
        );
332
      }
333
      return next();
×
334
    };
335
  }
336

337
  private applyVersionFilter<T extends HttpServer>(
338
    router: T,
339
    routePathMetadata: RoutePathMetadata,
340
    handler: Function,
341
  ) {
342
    const version = this.routePathFactory.getVersion(routePathMetadata)!;
1✔
343
    return router.applyVersionFilter(
1✔
344
      handler,
345
      version,
346
      routePathMetadata.versioningOptions!,
347
    );
348
  }
349

350
  private createCallbackProxy(
351
    instance: Controller,
352
    callback: RouterProxyCallback,
353
    methodName: string,
354
    moduleRef: string,
355
    requestMethod: RequestMethod,
356
    contextId = STATIC_CONTEXT,
4✔
357
    inquirerId?: string,
358
  ) {
359
    const executionContext = this.executionContextCreator.create(
4✔
360
      instance,
361
      callback,
362
      methodName,
363
      moduleRef,
364
      requestMethod,
365
      contextId,
366
      inquirerId,
367
    );
368
    const exceptionFilter = this.exceptionsFilter.create(
4✔
369
      instance,
370
      callback,
371
      moduleRef,
372
      contextId,
373
      inquirerId,
374
    );
375
    return this.routerProxy.createProxy(executionContext, exceptionFilter);
4✔
376
  }
377

378
  public createRequestScopedHandler(
379
    instanceWrapper: InstanceWrapper,
380
    requestMethod: RequestMethod,
381
    moduleRef: Module,
382
    moduleKey: string,
383
    methodName: string,
384
  ) {
385
    const { instance } = instanceWrapper;
1✔
386
    const collection = moduleRef.controllers;
1✔
387

388
    const isTreeDurable = instanceWrapper.isDependencyTreeDurable();
1✔
389

390
    return async <TRequest extends Record<any, any>, TResponse>(
1✔
391
      req: TRequest,
392
      res: TResponse,
393
      next: () => void,
394
    ) => {
395
      try {
1✔
396
        const contextId = this.getContextId(req, isTreeDurable);
1✔
397
        const contextInstance = await this.injector.loadPerContext(
×
398
          instance,
399
          moduleRef,
400
          collection,
401
          contextId,
402
        );
403
        await this.createCallbackProxy(
×
404
          contextInstance,
405
          contextInstance[methodName],
406
          methodName,
407
          moduleKey,
408
          requestMethod,
409
          contextId,
410
          instanceWrapper.id,
411
        )(req, res, next);
412
      } catch (err) {
413
        let exceptionFilter = this.exceptionFiltersCache.get(
1✔
414
          instance[methodName],
415
        );
416
        if (!exceptionFilter) {
1!
417
          exceptionFilter = this.exceptionsFilter.create(
1✔
418
            instance,
419
            instance[methodName],
420
            moduleKey,
421
          );
422
          this.exceptionFiltersCache.set(instance[methodName], exceptionFilter);
1✔
423
        }
424
        const host = new ExecutionContextHost([req, res, next]);
1✔
425
        exceptionFilter.next(err, host);
1✔
426
      }
427
    };
428
  }
429

430
  private getContextId<T extends Record<any, unknown> = any>(
431
    request: T,
432
    isTreeDurable: boolean,
433
  ): ContextId {
434
    const contextId = ContextIdFactory.getByRequest(request);
1✔
435
    if (!request[REQUEST_CONTEXT_ID as any]) {
1!
436
      Object.defineProperty(request, REQUEST_CONTEXT_ID, {
×
437
        value: contextId,
438
        enumerable: false,
439
        writable: false,
440
        configurable: false,
441
      });
442

443
      const requestProviderValue = isTreeDurable
×
444
        ? contextId.payload
×
445
        : Object.assign(request, contextId.payload);
446
      this.container.registerRequestProvider(requestProviderValue, contextId);
×
447
    }
448
    return contextId;
×
449
  }
450

451
  private copyMetadataToCallback(
452
    originalCallback: RouterProxyCallback,
453
    targetCallback: Function,
454
  ) {
455
    for (const key of Reflect.getMetadataKeys(originalCallback)) {
5✔
456
      Reflect.defineMetadata(
9✔
457
        key,
458
        Reflect.getMetadata(key, originalCallback),
459
        targetCallback,
460
      );
461
    }
462
  }
463
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc