@nestjs/platform-fastify 12.1.2 → 12.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,12 @@ import { HttpException, HttpStatus, Logger, StreamableFile, VERSION_NEUTRAL, Ver
2
2
  import { fastify, } from 'fastify';
3
3
  import Reply from 'fastify/lib/reply.js';
4
4
  import fastifySymbols from 'fastify/lib/symbols.js';
5
+ import * as http from 'http';
5
6
  import * as net from 'net';
6
7
  import { pathToRegexp } from 'path-to-regexp';
8
+ import { Readable } from 'stream';
7
9
  import middie from '@fastify/middie';
8
- import { loadPackage, tryLoadPackage, isNil, isString, isUndefined, } from '@nestjs/common/internal';
10
+ import { loadPackage, tryLoadPackage, isFunction, isNil, isString, isUndefined, } from '@nestjs/common/internal';
9
11
  import { AbstractHttpAdapter } from '@nestjs/core';
10
12
  import { LegacyRouteConverter } from '@nestjs/core/internal';
11
13
  import { getMediaTypeVersion } from './utils/get-media-type-version.util.js';
@@ -37,15 +39,26 @@ export class FastifyAdapter extends AbstractHttpAdapter {
37
39
  _pathPrefix;
38
40
  openConnections = new Set();
39
41
  isClosing = false;
42
+ isShuttingDown = false;
43
+ return503OnClosing = false;
40
44
  _isParserRegistered;
45
+ // Fastify's `hasContentTypeParser('application/json')` is always true (built-in
46
+ // parser), so the types registered through `useBodyParser()` are tracked here.
47
+ registeredContentTypes = new Set();
41
48
  onRequestHook;
42
49
  onResponseHook;
50
+ securityHook;
43
51
  isMiddieRegistered;
44
52
  multipartMode = 'auto';
45
53
  multipartOptions = {};
46
54
  isMultipartRequested = false;
47
55
  pendingMiddlewares = [];
56
+ // The encapsulated contexts created since middie was registered (see
57
+ // `registerMiddie()`)
58
+ middieContexts = [];
48
59
  versioningOptions;
60
+ // Exceptions thrown by the extractor of custom versioning, by raw request
61
+ versionExtractorErrors = new WeakMap();
49
62
  versionConstraint = {
50
63
  name: 'version',
51
64
  validate(value) {
@@ -103,7 +116,16 @@ export class FastifyAdapter extends AbstractHttpAdapter {
103
116
  }
104
117
  // Custom Versioning Handler
105
118
  else if (this.versioningOptions?.type === VersioningType.CUSTOM) {
106
- return this.versioningOptions.extractor(req);
119
+ try {
120
+ return this.versioningOptions.extractor(req);
121
+ }
122
+ catch (err) {
123
+ // Fastify routes the request, and so calls the extractor, before
124
+ // anything could catch the exception. It is passed to the error
125
+ // handler from an `onRequest` hook instead (see the constructor).
126
+ this.versionExtractorErrors.set(req, err);
127
+ return undefined;
128
+ }
107
129
  }
108
130
  return undefined;
109
131
  },
@@ -117,11 +139,28 @@ export class FastifyAdapter extends AbstractHttpAdapter {
117
139
  const instance = instanceOrOptions && instanceOrOptions.server
118
140
  ? instanceOrOptions
119
141
  : fastify({
142
+ // The router answers a malformed URL (400) or an over-long route
143
+ // parameter (414) before any hook runs, so the security hook (see
144
+ // `registerSecurityHook()`) runs here, for its headers.
145
+ frameworkErrors: (error, request, reply) => {
146
+ try {
147
+ this.securityHook?.(request, reply.raw);
148
+ }
149
+ catch {
150
+ // The request is rejected either way, and a throw here would
151
+ // escape the router. `request.params` and `request.query`
152
+ // are null, so a CSRF `exclude` predicate may throw.
153
+ }
154
+ reply.send(error);
155
+ },
120
156
  ...instanceOrOptions,
121
157
  routerOptions: {
122
158
  ...this.getTopLevelRouterOptions(instanceOrOptions),
123
159
  ...instanceOrOptions?.routerOptions,
124
160
  constraints: {
161
+ ...instanceOrOptions?.constraints,
162
+ ...instanceOrOptions?.routerOptions
163
+ ?.constraints,
125
164
  version: this.versionConstraint,
126
165
  },
127
166
  },
@@ -141,21 +180,102 @@ export class FastifyAdapter extends AbstractHttpAdapter {
141
180
  }
142
181
  this.instance.addHook('onRequest', (request, reply, done) => {
143
182
  if (this.onRequestHook) {
144
- this.onRequestHook(request, reply, done);
183
+ this.runLifecycleHook(this.onRequestHook, request, reply, done);
145
184
  }
146
185
  else {
147
186
  done();
148
187
  }
149
188
  });
189
+ // Fastify's own `return503OnClosing` only applies once `close()` runs,
190
+ // which is after the shutdown hooks. Added here rather than in
191
+ // `initHttpServer()` so that it runs before Nest middleware:
192
+ // `NestFactory.create()` calls `init()`, which registers middie, first.
193
+ // Answers with the same body as Fastify's own 503, so that clients get
194
+ // one response shape for the whole shutdown.
195
+ this.instance.addHook('onRequest', (request, reply, done) => {
196
+ if (this.return503OnClosing && this.isShuttingDown) {
197
+ if (request.raw.httpVersionMajor !== 2) {
198
+ reply.header('Connection', 'close');
199
+ }
200
+ reply.code(503).send({
201
+ error: 'Service Unavailable',
202
+ message: 'Service Unavailable',
203
+ statusCode: 503,
204
+ });
205
+ }
206
+ else {
207
+ done();
208
+ }
209
+ });
210
+ // The request hook of `app.enableCsrfProtection()` and
211
+ // `app.useSecurityHeaders()` (see `registerSecurityHook()`). Added here
212
+ // for the same reason as the hook above: it must run before Nest
213
+ // middleware, and `NestFactory.create()` registers middie before either
214
+ // method can be called. After the `503` hook, as on Express.
215
+ this.instance.addHook('onRequest', (request, reply, done) => {
216
+ if (this.securityHook) {
217
+ done(this.securityHook(request, reply.raw));
218
+ }
219
+ else {
220
+ done();
221
+ }
222
+ });
223
+ // Passes requests whose target middleware cannot be matched against to
224
+ // the not-found handler (see `isRoutableRequestTarget()`). Added here,
225
+ // like the hooks above, so that it runs before Nest middleware, and
226
+ // after the security hook, so that the `404` carries its headers, as on
227
+ // Express. A request the router did not match is already on its way to
228
+ // the not-found handler.
229
+ this.instance.addHook('onRequest', (request, reply, done) => {
230
+ if (request.is404 || this.isRoutableRequestTarget(request.raw.url)) {
231
+ done();
232
+ }
233
+ else {
234
+ reply.callNotFound();
235
+ }
236
+ });
237
+ // Passes an exception thrown by the extractor of custom versioning while
238
+ // Fastify routed the request (see `versionConstraint`) to the error
239
+ // handler, for matched and unmatched routes alike. Added here, like the
240
+ // hooks above, so that it runs before Nest middleware.
241
+ this.instance.addHook('onRequest', (request, _reply, done) => {
242
+ if (!this.versionExtractorErrors.has(request.raw)) {
243
+ done();
244
+ return;
245
+ }
246
+ const err = this.versionExtractorErrors.get(request.raw);
247
+ this.versionExtractorErrors.delete(request.raw);
248
+ done(err || new fastify.errorCodes.FST_ERR_SEND_UNDEFINED_ERR());
249
+ });
150
250
  this.instance.addHook('onResponse', (request, reply, done) => {
151
251
  if (this.onResponseHook) {
152
- this.onResponseHook(request, reply, done);
252
+ this.runLifecycleHook(this.onResponseHook, request, reply, done);
153
253
  }
154
254
  else {
155
255
  done();
156
256
  }
157
257
  });
158
258
  }
259
+ runLifecycleHook(hook, request, reply, done) {
260
+ let completed = false;
261
+ const doneOnce = (err) => {
262
+ if (completed)
263
+ return;
264
+ completed = true;
265
+ done(err);
266
+ };
267
+ try {
268
+ // Do not return the promise to Fastify: both completion styles share
269
+ // this guard, including hooks that call done and return a promise.
270
+ const result = hook.call(this, request, reply, doneOnce);
271
+ if (result && typeof result.then === 'function') {
272
+ result.then(() => doneOnce(), err => doneOnce(err || new fastify.errorCodes.FST_ERR_SEND_UNDEFINED_ERR()));
273
+ }
274
+ }
275
+ catch (err) {
276
+ doneOnce(err);
277
+ }
278
+ }
159
279
  setOnRequestHook(hook) {
160
280
  this.onRequestHook = hook;
161
281
  }
@@ -181,6 +301,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
181
301
  listen(listenOptions, ...args) {
182
302
  const isFirstArgTypeofFunction = typeof args[0] === 'function';
183
303
  const callback = isFirstArgTypeofFunction ? args[0] : args[1];
304
+ if (typeof listenOptions === 'string' && Number.isNaN(+listenOptions)) {
305
+ // A string that is not a number is a Unix socket path or a named pipe.
306
+ // The host does not apply to it.
307
+ return this.instance.listen({ path: listenOptions }, callback);
308
+ }
184
309
  let options;
185
310
  if (typeof listenOptions === 'object' &&
186
311
  (listenOptions.host !== undefined ||
@@ -197,8 +322,23 @@ export class FastifyAdapter extends AbstractHttpAdapter {
197
322
  if (!isFirstArgTypeofFunction) {
198
323
  options.host = args[0];
199
324
  }
325
+ if (options.port !== undefined && !this.isValidPort(options.port)) {
326
+ // Fastify calls `server.listen()` from its ready callback, where the
327
+ // RangeError Node throws for a bad port becomes an unhandled rejection
328
+ // and the callback never runs. Throw right away instead, as the
329
+ // `http.Server` of the Express adapter does.
330
+ const error = new RangeError(`Options.port should be >= 0 and < 65536. Received ${String(options.port)}.`);
331
+ throw Object.assign(error, { code: 'ERR_SOCKET_BAD_PORT' });
332
+ }
200
333
  return this.instance.listen(options, callback);
201
334
  }
335
+ isValidPort(port) {
336
+ const value = typeof port === 'string' && port.trim() !== '' ? Number(port) : port;
337
+ return (typeof value === 'number' &&
338
+ Number.isInteger(value) &&
339
+ value >= 0 &&
340
+ value <= 65535);
341
+ }
202
342
  get(...args) {
203
343
  return this.injectRouteOptions('GET', ...args);
204
344
  }
@@ -247,6 +387,16 @@ export class FastifyAdapter extends AbstractHttpAdapter {
247
387
  unlock(...args) {
248
388
  return this.injectRouteOptions('UNLOCK', ...args);
249
389
  }
390
+ all(...args) {
391
+ // Routes registered by Nest ("path, handler") get the route options
392
+ // (e.g. the version), with the same methods as "fastify.all()".
393
+ if (args.length === 2 && isFunction(args[1])) {
394
+ return this.injectRouteOptions(this.instance.supportedMethods, ...args);
395
+ }
396
+ const handlerRef = args[args.length - 1];
397
+ args[args.length - 1] = this.requireNonEmptyParams(args[0], handlerRef);
398
+ return this.instance.all(...args);
399
+ }
250
400
  applyVersionFilter(handler, version, versioningOptions) {
251
401
  if (!this.versioningOptions) {
252
402
  this.versioningOptions = versioningOptions;
@@ -271,6 +421,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
271
421
  fastifyReply.status(statusCode);
272
422
  }
273
423
  if (body instanceof StreamableFile) {
424
+ const stream = body.getStream();
425
+ // Once the reply was sent (e.g. by `handlerTimeout`), send() drops the
426
+ // stream without consuming or destroying it.
427
+ if (fastifyReply.sent && isFunction(stream?.destroy)) {
428
+ stream.destroy();
429
+ }
274
430
  const streamHeaders = body.getHeaders();
275
431
  if (fastifyReply.getHeader('Content-Type') === undefined &&
276
432
  streamHeaders.type !== undefined) {
@@ -284,7 +440,29 @@ export class FastifyAdapter extends AbstractHttpAdapter {
284
440
  streamHeaders.length !== undefined) {
285
441
  fastifyReply.header('Content-Length', streamHeaders.length);
286
442
  }
287
- body = body.getStream();
443
+ body = stream;
444
+ // Fastify's HEAD route handler resumes a stream payload, and a route
445
+ // declared for HEAD (or a raw response) pipes it into a response that
446
+ // discards it, so the whole source would be read for nothing. Fastify
447
+ // gets an empty stream instead, as an empty body would reset
448
+ // Content-Length to 0.
449
+ if (fastifyReply.raw?.req?.method === 'HEAD' &&
450
+ isFunction(stream?.destroy)) {
451
+ stream.destroy();
452
+ body = Readable.from([]);
453
+ }
454
+ }
455
+ // Fastify passes an Error payload to the error handler instead of
456
+ // sending it, and the reply around a raw response (e.g. the one
457
+ // middleware gets) has none. An Error body sent with an error status,
458
+ // as for `new UnauthorizedException(err)`, gets the standard body of the
459
+ // status instead, without the properties of the Error.
460
+ if (body instanceof Error &&
461
+ fastifyReply.statusCode >= HttpStatus.BAD_REQUEST) {
462
+ body = {
463
+ statusCode: fastifyReply.statusCode,
464
+ message: http.STATUS_CODES[fastifyReply.statusCode] ?? 'Error',
465
+ };
288
466
  }
289
467
  const responseContentType = fastifyReply.getHeader('Content-Type');
290
468
  if (typeof responseContentType === 'string' &&
@@ -303,7 +481,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
303
481
  return response.code(statusCode);
304
482
  }
305
483
  end(response, message) {
306
- response.raw.end(message);
484
+ if (this.isNativeResponse(response)) {
485
+ response.end(message);
486
+ return;
487
+ }
488
+ const reply = response;
489
+ reply.raw.end(message);
307
490
  }
308
491
  render(response, view, options) {
309
492
  return response && response.view(view, options);
@@ -379,7 +562,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
379
562
  inject(opts) {
380
563
  return this.instance.inject(opts);
381
564
  }
565
+ beforeClose() {
566
+ this.isShuttingDown = true;
567
+ }
382
568
  async close() {
569
+ this.isShuttingDown = true;
383
570
  this.isClosing = true;
384
571
  try {
385
572
  this.closeOpenConnections();
@@ -395,6 +582,7 @@ export class FastifyAdapter extends AbstractHttpAdapter {
395
582
  }
396
583
  initHttpServer(options = {}) {
397
584
  this.httpServer = this.instance.server;
585
+ this.return503OnClosing = !!options?.return503OnClosing;
398
586
  if (options?.forceCloseConnections) {
399
587
  this.trackOpenConnections();
400
588
  }
@@ -415,26 +603,52 @@ export class FastifyAdapter extends AbstractHttpAdapter {
415
603
  return this.register(loadPackage('@fastify/view', 'FastifyAdapter.setViewEngine()', () => import('@fastify/view')), options);
416
604
  }
417
605
  isHeadersSent(response) {
418
- return response.sent;
606
+ if (this.isNativeResponse(response)) {
607
+ return response.headersSent;
608
+ }
609
+ const reply = response;
610
+ // `reply.sent` only covers hijacked or ended replies, not headers
611
+ // already flushed through `reply.raw`.
612
+ return reply.sent || reply.raw.headersSent;
419
613
  }
420
614
  getHeader(response, name) {
421
615
  return response.getHeader(name);
422
616
  }
423
617
  setHeader(response, name, value) {
424
- return response.header(name, value);
618
+ if (this.isNativeResponse(response)) {
619
+ return response.setHeader(name, value);
620
+ }
621
+ const reply = response;
622
+ return reply.header(name, value);
425
623
  }
426
624
  appendHeader(response, name, value) {
427
- const current = response.getHeader(name);
428
- if (current === undefined) {
429
- return response.header(name, value);
625
+ if (this.isNativeResponse(response)) {
626
+ const current = response.getHeader(name);
627
+ return response.setHeader(name, current === undefined ? value : [current, value].flat().map(String));
430
628
  }
431
- // Fastify Reply.header() already concatenates set-cookie.
432
- // Re-passing the accumulated list would duplicate previous cookies.
629
+ const reply = response;
630
+ // A set-cookie value can live on the reply or only on the raw response
631
+ // (e.g., set by a middleware). Reply.getHeader() reads both, and the
632
+ // merged list is written to both: reply.send() sends the reply headers,
633
+ // while a hijacked reply or a handler that ends reply.raw sends the raw
634
+ // ones.
433
635
  if (String(name).toLowerCase() === 'set-cookie') {
434
- return response.header(name, value);
636
+ const current = reply.getHeader(name);
637
+ const values = current === undefined ? [value] : [current, value].flat().map(String);
638
+ reply.removeHeader(name);
639
+ reply.header(name, values);
640
+ if (!reply.raw.headersSent) {
641
+ // A copy, so a later reply.header() call does not change the raw header
642
+ reply.raw.setHeader(name, [...values]);
643
+ }
644
+ return reply;
645
+ }
646
+ const current = reply.getHeader(name);
647
+ if (current === undefined) {
648
+ return reply.header(name, value);
435
649
  }
436
650
  const values = Array.isArray(current) ? current : [current];
437
- return response.header(name, values.concat(value));
651
+ return reply.header(name, values.concat(value));
438
652
  }
439
653
  getRequestHostname(request) {
440
654
  return request.hostname;
@@ -450,19 +664,18 @@ export class FastifyAdapter extends AbstractHttpAdapter {
450
664
  }
451
665
  /**
452
666
  * Runs the request hook of the built-in HTTP security features in an
453
- * `onRequest` hook: before middie (Nest middleware), content-type parsing,
454
- * guards and handlers, and also for unmatched routes. Headers set by the
455
- * hook go to the Node.js response (`reply.raw`): Fastify merges them into
456
- * every response it sends, errors and `404`s included, while
667
+ * `onRequest` hook added by the constructor: before middie (Nest middleware
668
+ * and `app.use()` middleware, whenever they are registered), content-type
669
+ * parsing, guards and handlers, and also for unmatched routes. Headers set
670
+ * by the hook go to the Node.js response (`reply.raw`): Fastify merges them
671
+ * into every response it sends, errors and `404`s included, while
457
672
  * `reply.header()` / `@Header()` values take precedence, and responses
458
- * written to `reply.raw` directly (e.g. `@Sse()`) carry them too. A
459
- * rejection goes to `done(error)`, i.e. to the Nest exception layer
673
+ * written to `reply.raw` directly (e.g. `@Sse()`, or middleware) carry them
674
+ * too. A rejection goes to `done(error)`, i.e. to the Nest exception layer
460
675
  * installed with `setErrorHandler()`.
461
676
  */
462
677
  registerSecurityHook(hook) {
463
- this.instance.addHook('onRequest', (request, reply, done) => {
464
- done(hook(request, reply.raw));
465
- });
678
+ this.securityHook = hook;
466
679
  }
467
680
  registerParserMiddleware(prefix, rawBody) {
468
681
  if (this._isParserRegistered) {
@@ -478,23 +691,29 @@ export class FastifyAdapter extends AbstractHttpAdapter {
478
691
  : undefined;
479
692
  }
480
693
  useBodyParser(type, rawBody, options, parser) {
694
+ if (Array.isArray(type)) {
695
+ // One by one, so that each content type gets its own default parser.
696
+ for (const contentType of type) {
697
+ this.useBodyParser(contentType, rawBody, options, parser);
698
+ }
699
+ return;
700
+ }
481
701
  const parserOptions = {
482
702
  ...(options || {}),
483
703
  parseAs: 'buffer',
484
704
  };
705
+ const bodyParser = parser ?? this.getDefaultBodyParser(type);
485
706
  this.getInstance().addContentTypeParser(type, parserOptions, (req, body, done) => {
486
707
  if (rawBody === true && Buffer.isBuffer(body)) {
487
708
  req.rawBody = body;
488
709
  }
489
- if (parser) {
490
- parser(req, body, done);
710
+ if (bodyParser) {
711
+ bodyParser(req, body, done);
491
712
  return;
492
713
  }
493
714
  done(null, body);
494
715
  });
495
- // To avoid the Nest application init to override our custom
496
- // body parser, we mark the parsers as registered.
497
- this._isParserRegistered = true;
716
+ this.registeredContentTypes.add(this.normalizeContentType(type));
498
717
  }
499
718
  async createMiddlewareFactory(requestMethod) {
500
719
  if (!this.isMiddieRegistered) {
@@ -520,9 +739,13 @@ export class FastifyAdapter extends AbstractHttpAdapter {
520
739
  re = hasEndOfStringCharacter
521
740
  ? new RegExp(re.source + '$', re.flags)
522
741
  : re;
523
- // The following type assertion is valid as we use import('@fastify/middie') rather than require('@fastify/middie')
524
- // ref https://github.com/fastify/middie/pull/55
525
- this.instance.use(normalizedPath, (req, res, next) => {
742
+ // A case-insensitive router lower-cases the decoded path, where some
743
+ // characters only fold to ASCII ("%E2%84%AA", KELVIN SIGN, is routed
744
+ // as "k"). The RegExp of @fastify/middie does not fold them, so on
745
+ // such a router the middleware is registered for every path and the
746
+ // path is only matched below.
747
+ const { caseSensitive } = this.getRouteMatchingOptions();
748
+ const createMiddleware = (stripsPath) => (req, res, next) => {
526
749
  const queryParamsIndex = req.originalUrl.indexOf('?');
527
750
  let pathname = queryParamsIndex >= 0
528
751
  ? req.originalUrl.slice(0, queryParamsIndex)
@@ -535,9 +758,28 @@ export class FastifyAdapter extends AbstractHttpAdapter {
535
758
  if (!re.exec(pathToCheck)) {
536
759
  return next();
537
760
  }
761
+ if (stripsPath) {
762
+ // Strips the path from "req.url", as @fastify/middie does
763
+ // when it matches the path itself
764
+ const suffixIndex = req.originalUrl.search(/[?#]/);
765
+ req.url =
766
+ suffixIndex >= 0
767
+ ? `/${req.originalUrl.slice(suffixIndex)}`
768
+ : '/';
769
+ }
538
770
  }
539
- return callback(req, res, next);
540
- });
771
+ this.runMiddleware(callback, req, res, next);
772
+ };
773
+ // The following type assertion is valid as we use import('@fastify/middie') rather than require('@fastify/middie')
774
+ // ref https://github.com/fastify/middie/pull/55
775
+ this.instance.use(caseSensitive ? normalizedPath : '/', createMiddleware(!caseSensitive));
776
+ // The contexts created since middie was registered have a copy of the
777
+ // middleware of their parent without this one (see `registerMiddie()`).
778
+ // It is registered there without its path, which middie would prefix
779
+ // with the one of the context.
780
+ for (const context of this.middieContexts) {
781
+ context.use(createMiddleware(true));
782
+ }
541
783
  }
542
784
  catch (e) {
543
785
  if (e instanceof TypeError) {
@@ -547,12 +789,53 @@ export class FastifyAdapter extends AbstractHttpAdapter {
547
789
  }
548
790
  };
549
791
  }
792
+ runMiddleware(middleware, req, res, next) {
793
+ let completed = false;
794
+ const nextOnce = (err) => {
795
+ if (completed)
796
+ return;
797
+ completed = true;
798
+ next(err);
799
+ };
800
+ try {
801
+ // middie discards the return value, so the promise is observed here:
802
+ // the proxies of `RouterProxy` reject when the exception layer throws.
803
+ // Its resolution is not passed to `next`, as a middleware that answers
804
+ // the request does not call it.
805
+ const result = middleware(req, res, nextOnce);
806
+ if (result && typeof result.then === 'function') {
807
+ result.then(undefined, (err) => {
808
+ if (completed) {
809
+ this.logger.error(err);
810
+ return;
811
+ }
812
+ nextOnce(err || new fastify.errorCodes.FST_ERR_SEND_UNDEFINED_ERR());
813
+ });
814
+ }
815
+ }
816
+ catch (err) {
817
+ nextOnce(err);
818
+ }
819
+ }
550
820
  getType() {
551
821
  return 'fastify';
552
822
  }
553
823
  isRouteOrderSensitive() {
554
824
  return false;
555
825
  }
826
+ // The return type is spelled out (rather than "RouteMatchingOptions") so
827
+ // that the declarations still compile against an older "@nestjs/common"
828
+ getRouteMatchingOptions() {
829
+ const initialConfig = this.instance.initialConfig;
830
+ // Absent when a Fastify instance created without it is passed in
831
+ const routerOptions = (initialConfig.routerOptions ??
832
+ {});
833
+ return {
834
+ caseSensitive: !(routerOptions.caseSensitive === false ||
835
+ initialConfig.caseSensitive === false),
836
+ ignoreTrailingSlash: !!(routerOptions.ignoreTrailingSlash || initialConfig.ignoreTrailingSlash),
837
+ };
838
+ }
556
839
  use(...args) {
557
840
  // Fastify requires @fastify/middie plugin to be registered before middleware can be used.
558
841
  // If middie is not registered yet, we queue the middleware and register it later during init.
@@ -582,25 +865,69 @@ export class FastifyAdapter extends AbstractHttpAdapter {
582
865
  }
583
866
  registerJsonContentParser(rawBody) {
584
867
  const contentType = 'application/json';
868
+ if (this.hasCustomBodyParser(contentType)) {
869
+ return;
870
+ }
585
871
  const withRawBody = !!rawBody;
586
872
  const { bodyLimit } = this.getInstance().initialConfig;
587
- this.useBodyParser(contentType, withRawBody, { bodyLimit }, (req, body, done) => {
588
- const { onProtoPoisoning, onConstructorPoisoning } = this.instance.initialConfig;
589
- const defaultJsonParser = this.instance.getDefaultJsonParser(onProtoPoisoning || 'error', onConstructorPoisoning || 'error');
590
- defaultJsonParser(req, body, done);
591
- });
873
+ this.useBodyParser(contentType, withRawBody, { bodyLimit });
592
874
  }
593
875
  registerUrlencodedContentParser(rawBody) {
594
876
  const contentType = 'application/x-www-form-urlencoded';
877
+ if (this.hasCustomBodyParser(contentType)) {
878
+ return;
879
+ }
595
880
  const withRawBody = !!rawBody;
596
881
  const { bodyLimit } = this.getInstance().initialConfig;
597
- this.useBodyParser(contentType, withRawBody, { bodyLimit }, (_req, body, done) => {
598
- done(null, querystringParse(body.toString()));
599
- });
882
+ this.useBodyParser(contentType, withRawBody, { bodyLimit });
883
+ }
884
+ // A parser registered through `useBodyParser()` takes precedence over the
885
+ // default one, including a RegExp or catch-all ('*') parser matching the type.
886
+ hasCustomBodyParser(contentType) {
887
+ for (const type of this.registeredContentTypes) {
888
+ if (type === '*' || type === contentType) {
889
+ return true;
890
+ }
891
+ if (isString(type)) {
892
+ continue;
893
+ }
894
+ type.lastIndex = 0;
895
+ if (type.test(contentType)) {
896
+ return true;
897
+ }
898
+ }
899
+ return false;
900
+ }
901
+ // Fastify stores string content types trimmed and lower-cased.
902
+ normalizeContentType(type) {
903
+ return isString(type) ? type.trim().toLowerCase() : type;
904
+ }
905
+ getDefaultBodyParser(type) {
906
+ switch (this.normalizeContentType(type)) {
907
+ case 'application/json':
908
+ return (req, body, done) => {
909
+ const { onProtoPoisoning, onConstructorPoisoning } = this.instance.initialConfig;
910
+ const defaultJsonParser = this.instance.getDefaultJsonParser(onProtoPoisoning || 'error', onConstructorPoisoning || 'error');
911
+ defaultJsonParser(req, body, done);
912
+ };
913
+ case 'application/x-www-form-urlencoded':
914
+ return (_req, body, done) => {
915
+ done(null, querystringParse(body.toString()));
916
+ };
917
+ }
600
918
  }
601
919
  async registerMiddie() {
602
920
  this.isMiddieRegistered = true;
603
921
  await this.register(middie);
922
+ // middie gives each encapsulated context created from now on a copy of
923
+ // the middleware of its parent, as it is at that time. As
924
+ // `NestFactory.create()` registers middie before `main.ts` runs, a
925
+ // plugin registered there with an awaited `register()` gets a context
926
+ // without the Nest middleware `app.init()` registers later, so
927
+ // `createMiddlewareFactory()` adds it to the contexts tracked here.
928
+ this.instance.addHook('onRegister', instance => {
929
+ this.middieContexts.push(instance);
930
+ });
604
931
  }
605
932
  getRequestOriginalUrl(rawRequest) {
606
933
  return rawRequest.originalUrl || rawRequest.url;
@@ -618,9 +945,10 @@ export class FastifyAdapter extends AbstractHttpAdapter {
618
945
  const routeToInject = {
619
946
  method: routerMethodKey,
620
947
  url: args[0],
621
- handler: handlerRef,
948
+ handler: this.requireNonEmptyParams(args[0], handlerRef),
622
949
  };
623
- if (!this.instance.supportedMethods.includes(routerMethodKey)) {
950
+ if (isString(routerMethodKey) &&
951
+ !this.instance.supportedMethods.includes(routerMethodKey)) {
624
952
  this.instance.addHttpMethod(routerMethodKey, { hasBody: true });
625
953
  }
626
954
  if (isVersioned || hasConstraints || hasConfig || hasSchema) {
@@ -649,6 +977,35 @@ export class FastifyAdapter extends AbstractHttpAdapter {
649
977
  }
650
978
  return this.instance.route(routeToInject);
651
979
  }
980
+ /**
981
+ * "find-my-way" matches an empty path segment for a route parameter
982
+ * ("/users//profile" -> { id: "" }), while "path-to-regexp", which
983
+ * middleware paths (and the ExpressAdapter's routes) are matched with,
984
+ * needs at least one character per parameter. Requests with an empty route
985
+ * parameter are passed to the not-found handler instead, as with the
986
+ * ExpressAdapter, so that middleware and routes always match the same
987
+ * requests. Only the wildcard ("*") may be empty.
988
+ */
989
+ requireNonEmptyParams(url, handler) {
990
+ if (isString(url) && !url.includes(':')) {
991
+ return handler;
992
+ }
993
+ const routeHandler = function (request, reply) {
994
+ const params = request.params;
995
+ for (const name in params) {
996
+ if (params[name] === '' && name !== '*') {
997
+ reply.callNotFound();
998
+ return;
999
+ }
1000
+ }
1001
+ return handler.call(this, request, reply);
1002
+ };
1003
+ Object.defineProperty(routeHandler, 'name', {
1004
+ configurable: true,
1005
+ value: handler.name,
1006
+ });
1007
+ return routeHandler;
1008
+ }
652
1009
  /**
653
1010
  * Fastify still accepts the router options ("ignoreTrailingSlash",
654
1011
  * "caseSensitive", ...) at the top level, but "initialConfig.routerOptions"
@@ -677,7 +1034,8 @@ export class FastifyAdapter extends AbstractHttpAdapter {
677
1034
  }
678
1035
  sanitizeUrl(url) {
679
1036
  const initialConfig = this.instance.initialConfig;
680
- const routerOptions = initialConfig.routerOptions;
1037
+ const routerOptions = (initialConfig.routerOptions ??
1038
+ {});
681
1039
  // Absolute-form request targets ("GET http://host/path HTTP/1.1") must be
682
1040
  // resolved to their path before any other normalization, as the Fastify
683
1041
  // router does, so that middleware and routes always match the same path.
@@ -686,16 +1044,15 @@ export class FastifyAdapter extends AbstractHttpAdapter {
686
1044
  initialConfig.ignoreDuplicateSlashes) {
687
1045
  url = this.removeDuplicateSlashes(url);
688
1046
  }
689
- if (routerOptions.ignoreTrailingSlash ||
690
- initialConfig.ignoreTrailingSlash) {
1047
+ const { caseSensitive, ignoreTrailingSlash } = this.getRouteMatchingOptions();
1048
+ if (ignoreTrailingSlash) {
691
1049
  url = this.trimLastSlash(url);
692
1050
  }
693
- if (routerOptions.caseSensitive === false ||
694
- initialConfig.caseSensitive === false) {
695
- url = url.toLowerCase();
696
- }
697
- return safeDecodeURI(url, routerOptions.useSemicolonDelimiter ||
698
- initialConfig.useSemicolonDelimiter).path;
1051
+ const { path } = safeDecodeURI(url, routerOptions.useSemicolonDelimiter ||
1052
+ initialConfig.useSemicolonDelimiter);
1053
+ // Lower-cased once decoded, as the router does: "%E2%84%AA" (KELVIN
1054
+ // SIGN) only lower-cases to "k" then
1055
+ return caseSensitive ? path : path.toLowerCase();
699
1056
  }
700
1057
  removeDuplicateSlashes(path) {
701
1058
  const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
@@ -734,6 +1091,26 @@ export class FastifyAdapter extends AbstractHttpAdapter {
734
1091
  }
735
1092
  return pathStart === -1 ? '/' : url.slice(pathStart);
736
1093
  }
1094
+ /**
1095
+ * Whether the request target is in origin-form ("/users") or in the
1096
+ * absolute-form of an "http(s)" URL ("http://host/users"), the only forms
1097
+ * middleware paths can match.
1098
+ *
1099
+ * Node.js accepts other targets too (e.g. "GET *users HTTP/1.1").
1100
+ * "find-my-way" routes those as if their first character were the leading
1101
+ * "/" ("*users" is served by the "/users" route), while no middleware path
1102
+ * matches them, so the route would run without its middleware. They are
1103
+ * answered by the not-found handler instead, as with the ExpressAdapter,
1104
+ * whose router never matches a path that does not start with "/".
1105
+ *
1106
+ * That includes the asterisk-form "OPTIONS *" (RFC 9112, section 3.2.4),
1107
+ * which asks about the server as a whole rather than about a resource:
1108
+ * Nest has no handler for it, and "find-my-way" would serve it with the
1109
+ * "OPTIONS /" route.
1110
+ */
1111
+ isRoutableRequestTarget(url) {
1112
+ return !!url && this.getPathFromRequestTarget(url).charCodeAt(0) === 47;
1113
+ }
737
1114
  trackOpenConnections() {
738
1115
  const track = (socket) => {
739
1116
  if (this.isClosing) {