@nestjs/platform-fastify 12.1.1 → 12.1.3

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.
@@ -61,7 +61,10 @@ export declare class FastifyAdapter<TServer extends RawServerBase = RawServerDef
61
61
  protected _pathPrefix?: string;
62
62
  private readonly openConnections;
63
63
  private isClosing;
64
+ private isShuttingDown;
65
+ private return503OnClosing;
64
66
  private _isParserRegistered;
67
+ private readonly registeredContentTypes;
65
68
  private onRequestHook?;
66
69
  private onResponseHook?;
67
70
  private isMiddieRegistered;
@@ -73,11 +76,13 @@ export declare class FastifyAdapter<TServer extends RawServerBase = RawServerDef
73
76
  private readonly versionConstraint;
74
77
  get isParserRegistered(): boolean;
75
78
  constructor(instanceOrOptions?: TInstance | FastifyHttp2Options<any> | FastifyHttp2SecureOptions<any> | FastifyHttpsOptions<any> | FastifyHttpOptions<any> | FastifyAdapterBaseOptions<TServer>);
79
+ private runLifecycleHook;
76
80
  setOnRequestHook(hook: (request: TRequest, reply: TReply, done: (err?: Error) => void) => void | Promise<void>): void;
77
81
  setOnResponseHook(hook: (request: TRequest, reply: TReply, done: (err?: Error) => void) => void | Promise<void>): void;
78
82
  init(): Promise<void>;
79
83
  listen(port: string | number, callback?: () => void): void;
80
84
  listen(port: string | number, hostname: string, callback?: () => void): void;
85
+ private isValidPort;
81
86
  get(...args: any[]): FastifyInstance<TServer, TRawRequest, TRawResponse, FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>;
82
87
  post(...args: any[]): FastifyInstance<TServer, TRawRequest, TRawResponse, FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>;
83
88
  head(...args: any[]): FastifyInstance<TServer, TRawRequest, TRawResponse, FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>;
@@ -94,10 +99,11 @@ export declare class FastifyAdapter<TServer extends RawServerBase = RawServerDef
94
99
  move(...args: any[]): FastifyInstance<TServer, TRawRequest, TRawResponse, FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>;
95
100
  lock(...args: any[]): FastifyInstance<TServer, TRawRequest, TRawResponse, FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>;
96
101
  unlock(...args: any[]): FastifyInstance<TServer, TRawRequest, TRawResponse, FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>;
102
+ all(...args: any[]): any;
97
103
  applyVersionFilter(handler: Function, version: VersionValue, versioningOptions: VersioningOptions): VersionedRoute<TRequest, TReply>;
98
104
  reply(response: TRawResponse | TReply, body: any, statusCode?: number): FastifyReply<RouteGenericInterface, TServer, TRawRequest, TRawResponse, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
99
105
  status(response: TRawResponse | TReply, statusCode: number): any;
100
- end(response: TReply, message?: string): void;
106
+ end(response: TRawResponse | TReply, message?: string): void;
101
107
  render(response: TReply & {
102
108
  view: Function;
103
109
  }, view: string, options: any): FastifyReply<RouteGenericInterface, RawServerDefault, http.IncomingMessage, http.ServerResponse<http.IncomingMessage>, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
@@ -120,11 +126,12 @@ export declare class FastifyAdapter<TServer extends RawServerBase = RawServerDef
120
126
  useMultipart(): void;
121
127
  inject(): LightMyRequestChain;
122
128
  inject(opts: InjectOptions | string): Promise<LightMyRequestResponse>;
129
+ beforeClose(): void;
123
130
  close(): Promise<void>;
124
131
  initHttpServer(options?: NestApplicationOptions): void;
125
132
  useStaticAssets(options: FastifyStaticOptions): any;
126
133
  setViewEngine(options: FastifyViewOptions | string): any;
127
- isHeadersSent(response: TReply): boolean;
134
+ isHeadersSent(response: TRawResponse | TReply): boolean;
128
135
  getHeader(response: any, name: string): any;
129
136
  setHeader(response: TReply, name: string, value: string): FastifyReply<RouteGenericInterface, TServer, TRawRequest, TRawResponse, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
130
137
  appendHeader(response: any, name: string, value: string): any;
@@ -163,9 +170,22 @@ export declare class FastifyAdapter<TServer extends RawServerBase = RawServerDef
163
170
  private isNativeResponse;
164
171
  private registerJsonContentParser;
165
172
  private registerUrlencodedContentParser;
173
+ private hasCustomBodyParser;
174
+ private normalizeContentType;
175
+ private getDefaultBodyParser;
166
176
  private registerMiddie;
167
177
  private getRequestOriginalUrl;
168
178
  private injectRouteOptions;
179
+ /**
180
+ * "find-my-way" matches an empty path segment for a route parameter
181
+ * ("/users//profile" -> { id: "" }), while "path-to-regexp", which
182
+ * middleware paths (and the ExpressAdapter's routes) are matched with,
183
+ * needs at least one character per parameter. Requests with an empty route
184
+ * parameter are passed to the not-found handler instead, as with the
185
+ * ExpressAdapter, so that middleware and routes always match the same
186
+ * requests. Only the wildcard ("*") may be empty.
187
+ */
188
+ private requireNonEmptyParams;
169
189
  /**
170
190
  * Fastify still accepts the router options ("ignoreTrailingSlash",
171
191
  * "caseSensitive", ...) at the top level, but "initialConfig.routerOptions"
@@ -8,6 +8,7 @@ import middie from '@fastify/middie';
8
8
  import { loadPackage, tryLoadPackage, isNil, isString, isUndefined, } from '@nestjs/common/internal';
9
9
  import { AbstractHttpAdapter } from '@nestjs/core';
10
10
  import { LegacyRouteConverter } from '@nestjs/core/internal';
11
+ import { getMediaTypeVersion } from './utils/get-media-type-version.util.js';
11
12
  const { kRouteContext } = fastifySymbols;
12
13
  // Fastify uses `fast-querystring` internally to quickly parse URL query strings.
13
14
  import { parse as querystringParse } from 'fast-querystring';
@@ -22,6 +23,12 @@ function isFastifyMultipartPlugin(plugin) {
22
23
  (fn[Symbol.for('plugin-meta')]?.name === '@fastify/multipart' ||
23
24
  fn.name === 'fastifyMultipart'));
24
25
  }
26
+ // Covers "application/json" and the "+json" structured syntax suffix (RFC 6839),
27
+ // e.g. "application/problem+json", with or without parameters.
28
+ function isJsonContentType(contentType) {
29
+ const mediaType = contentType.split(';')[0].trim().toLowerCase();
30
+ return (mediaType.startsWith('application/json') || mediaType.endsWith('+json'));
31
+ }
25
32
  /**
26
33
  * @publicApi
27
34
  */
@@ -30,7 +37,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
30
37
  _pathPrefix;
31
38
  openConnections = new Set();
32
39
  isClosing = false;
40
+ isShuttingDown = false;
41
+ return503OnClosing = false;
33
42
  _isParserRegistered;
43
+ // Fastify's `hasContentTypeParser('application/json')` is always true (built-in
44
+ // parser), so the types registered through `useBodyParser()` are tracked here.
45
+ registeredContentTypes = new Set();
34
46
  onRequestHook;
35
47
  onResponseHook;
36
48
  isMiddieRegistered;
@@ -79,13 +91,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
79
91
  // Media Type (Accept Header) Versioning Handler
80
92
  if (this.versioningOptions?.type === VersioningType.MEDIA_TYPE) {
81
93
  const MEDIA_TYPE_HEADER = 'Accept';
82
- const acceptHeaderValue = (req.headers?.[MEDIA_TYPE_HEADER] || req.headers?.[MEDIA_TYPE_HEADER.toLowerCase()]);
83
- const acceptHeaderVersionParameter = acceptHeaderValue
84
- ? acceptHeaderValue.split(';')[1]
85
- : undefined;
86
- return isUndefined(acceptHeaderVersionParameter)
94
+ const acceptHeaderValue = req.headers?.[MEDIA_TYPE_HEADER] ||
95
+ req.headers?.[MEDIA_TYPE_HEADER.toLowerCase()];
96
+ const headerVersion = getMediaTypeVersion(acceptHeaderValue, this.versioningOptions.key);
97
+ return isUndefined(headerVersion)
87
98
  ? VERSION_NEUTRAL // No version was supplied
88
- : acceptHeaderVersionParameter.split(this.versioningOptions.key)[1];
99
+ : headerVersion;
89
100
  }
90
101
  // Header Versioning Handler
91
102
  else if (this.versioningOptions?.type === VersioningType.HEADER) {
@@ -116,6 +127,9 @@ export class FastifyAdapter extends AbstractHttpAdapter {
116
127
  ...this.getTopLevelRouterOptions(instanceOrOptions),
117
128
  ...instanceOrOptions?.routerOptions,
118
129
  constraints: {
130
+ ...instanceOrOptions?.constraints,
131
+ ...instanceOrOptions?.routerOptions
132
+ ?.constraints,
119
133
  version: this.versionConstraint,
120
134
  },
121
135
  },
@@ -135,7 +149,28 @@ export class FastifyAdapter extends AbstractHttpAdapter {
135
149
  }
136
150
  this.instance.addHook('onRequest', (request, reply, done) => {
137
151
  if (this.onRequestHook) {
138
- this.onRequestHook(request, reply, done);
152
+ this.runLifecycleHook(this.onRequestHook, request, reply, done);
153
+ }
154
+ else {
155
+ done();
156
+ }
157
+ });
158
+ // Fastify's own `return503OnClosing` only applies once `close()` runs,
159
+ // which is after the shutdown hooks. Added here rather than in
160
+ // `initHttpServer()` so that it runs before Nest middleware:
161
+ // `NestFactory.create()` calls `init()`, which registers middie, first.
162
+ // Answers with the same body as Fastify's own 503, so that clients get
163
+ // one response shape for the whole shutdown.
164
+ this.instance.addHook('onRequest', (request, reply, done) => {
165
+ if (this.return503OnClosing && this.isShuttingDown) {
166
+ if (request.raw.httpVersionMajor !== 2) {
167
+ reply.header('Connection', 'close');
168
+ }
169
+ reply.code(503).send({
170
+ error: 'Service Unavailable',
171
+ message: 'Service Unavailable',
172
+ statusCode: 503,
173
+ });
139
174
  }
140
175
  else {
141
176
  done();
@@ -143,13 +178,33 @@ export class FastifyAdapter extends AbstractHttpAdapter {
143
178
  });
144
179
  this.instance.addHook('onResponse', (request, reply, done) => {
145
180
  if (this.onResponseHook) {
146
- this.onResponseHook(request, reply, done);
181
+ this.runLifecycleHook(this.onResponseHook, request, reply, done);
147
182
  }
148
183
  else {
149
184
  done();
150
185
  }
151
186
  });
152
187
  }
188
+ runLifecycleHook(hook, request, reply, done) {
189
+ let completed = false;
190
+ const doneOnce = (err) => {
191
+ if (completed)
192
+ return;
193
+ completed = true;
194
+ done(err);
195
+ };
196
+ try {
197
+ // Do not return the promise to Fastify: both completion styles share
198
+ // this guard, including hooks that call done and return a promise.
199
+ const result = hook.call(this, request, reply, doneOnce);
200
+ if (result && typeof result.then === 'function') {
201
+ result.then(() => doneOnce(), err => doneOnce(err || new fastify.errorCodes.FST_ERR_SEND_UNDEFINED_ERR()));
202
+ }
203
+ }
204
+ catch (err) {
205
+ doneOnce(err);
206
+ }
207
+ }
153
208
  setOnRequestHook(hook) {
154
209
  this.onRequestHook = hook;
155
210
  }
@@ -175,6 +230,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
175
230
  listen(listenOptions, ...args) {
176
231
  const isFirstArgTypeofFunction = typeof args[0] === 'function';
177
232
  const callback = isFirstArgTypeofFunction ? args[0] : args[1];
233
+ if (typeof listenOptions === 'string' && Number.isNaN(+listenOptions)) {
234
+ // A string that is not a number is a Unix socket path or a named pipe.
235
+ // The host does not apply to it.
236
+ return this.instance.listen({ path: listenOptions }, callback);
237
+ }
178
238
  let options;
179
239
  if (typeof listenOptions === 'object' &&
180
240
  (listenOptions.host !== undefined ||
@@ -191,8 +251,23 @@ export class FastifyAdapter extends AbstractHttpAdapter {
191
251
  if (!isFirstArgTypeofFunction) {
192
252
  options.host = args[0];
193
253
  }
254
+ if (options.port !== undefined && !this.isValidPort(options.port)) {
255
+ // Fastify calls `server.listen()` from its ready callback, where the
256
+ // RangeError Node throws for a bad port becomes an unhandled rejection
257
+ // and the callback never runs. Throw right away instead, as the
258
+ // `http.Server` of the Express adapter does.
259
+ const error = new RangeError(`Options.port should be >= 0 and < 65536. Received ${String(options.port)}.`);
260
+ throw Object.assign(error, { code: 'ERR_SOCKET_BAD_PORT' });
261
+ }
194
262
  return this.instance.listen(options, callback);
195
263
  }
264
+ isValidPort(port) {
265
+ const value = typeof port === 'string' && port.trim() !== '' ? Number(port) : port;
266
+ return (typeof value === 'number' &&
267
+ Number.isInteger(value) &&
268
+ value >= 0 &&
269
+ value <= 65535);
270
+ }
196
271
  get(...args) {
197
272
  return this.injectRouteOptions('GET', ...args);
198
273
  }
@@ -241,6 +316,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
241
316
  unlock(...args) {
242
317
  return this.injectRouteOptions('UNLOCK', ...args);
243
318
  }
319
+ all(...args) {
320
+ const handlerRef = args[args.length - 1];
321
+ args[args.length - 1] = this.requireNonEmptyParams(args[0], handlerRef);
322
+ return this.instance.all(...args);
323
+ }
244
324
  applyVersionFilter(handler, version, versioningOptions) {
245
325
  if (!this.versioningOptions) {
246
326
  this.versioningOptions = versioningOptions;
@@ -282,7 +362,7 @@ export class FastifyAdapter extends AbstractHttpAdapter {
282
362
  }
283
363
  const responseContentType = fastifyReply.getHeader('Content-Type');
284
364
  if (typeof responseContentType === 'string' &&
285
- !responseContentType.startsWith('application/json') &&
365
+ !isJsonContentType(responseContentType) &&
286
366
  body?.statusCode >= HttpStatus.BAD_REQUEST) {
287
367
  Logger.warn("Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses", FastifyAdapter.name);
288
368
  fastifyReply.header('Content-Type', 'application/json');
@@ -297,7 +377,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
297
377
  return response.code(statusCode);
298
378
  }
299
379
  end(response, message) {
300
- response.raw.end(message);
380
+ if (this.isNativeResponse(response)) {
381
+ response.end(message);
382
+ return;
383
+ }
384
+ const reply = response;
385
+ reply.raw.end(message);
301
386
  }
302
387
  render(response, view, options) {
303
388
  return response && response.view(view, options);
@@ -373,7 +458,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
373
458
  inject(opts) {
374
459
  return this.instance.inject(opts);
375
460
  }
461
+ beforeClose() {
462
+ this.isShuttingDown = true;
463
+ }
376
464
  async close() {
465
+ this.isShuttingDown = true;
377
466
  this.isClosing = true;
378
467
  try {
379
468
  this.closeOpenConnections();
@@ -389,6 +478,7 @@ export class FastifyAdapter extends AbstractHttpAdapter {
389
478
  }
390
479
  initHttpServer(options = {}) {
391
480
  this.httpServer = this.instance.server;
481
+ this.return503OnClosing = !!options?.return503OnClosing;
392
482
  if (options?.forceCloseConnections) {
393
483
  this.trackOpenConnections();
394
484
  }
@@ -409,7 +499,13 @@ export class FastifyAdapter extends AbstractHttpAdapter {
409
499
  return this.register(loadPackage('@fastify/view', 'FastifyAdapter.setViewEngine()', () => import('@fastify/view')), options);
410
500
  }
411
501
  isHeadersSent(response) {
412
- return response.sent;
502
+ if (this.isNativeResponse(response)) {
503
+ return response.headersSent;
504
+ }
505
+ const reply = response;
506
+ // `reply.sent` only covers hijacked or ended replies, not headers
507
+ // already flushed through `reply.raw`.
508
+ return reply.sent || reply.raw.headersSent;
413
509
  }
414
510
  getHeader(response, name) {
415
511
  return response.getHeader(name);
@@ -472,23 +568,29 @@ export class FastifyAdapter extends AbstractHttpAdapter {
472
568
  : undefined;
473
569
  }
474
570
  useBodyParser(type, rawBody, options, parser) {
571
+ if (Array.isArray(type)) {
572
+ // One by one, so that each content type gets its own default parser.
573
+ for (const contentType of type) {
574
+ this.useBodyParser(contentType, rawBody, options, parser);
575
+ }
576
+ return;
577
+ }
475
578
  const parserOptions = {
476
579
  ...(options || {}),
477
580
  parseAs: 'buffer',
478
581
  };
582
+ const bodyParser = parser ?? this.getDefaultBodyParser(type);
479
583
  this.getInstance().addContentTypeParser(type, parserOptions, (req, body, done) => {
480
584
  if (rawBody === true && Buffer.isBuffer(body)) {
481
585
  req.rawBody = body;
482
586
  }
483
- if (parser) {
484
- parser(req, body, done);
587
+ if (bodyParser) {
588
+ bodyParser(req, body, done);
485
589
  return;
486
590
  }
487
591
  done(null, body);
488
592
  });
489
- // To avoid the Nest application init to override our custom
490
- // body parser, we mark the parsers as registered.
491
- this._isParserRegistered = true;
593
+ this.registeredContentTypes.add(this.normalizeContentType(type));
492
594
  }
493
595
  async createMiddlewareFactory(requestMethod) {
494
596
  if (!this.isMiddieRegistered) {
@@ -576,21 +678,56 @@ export class FastifyAdapter extends AbstractHttpAdapter {
576
678
  }
577
679
  registerJsonContentParser(rawBody) {
578
680
  const contentType = 'application/json';
681
+ if (this.hasCustomBodyParser(contentType)) {
682
+ return;
683
+ }
579
684
  const withRawBody = !!rawBody;
580
685
  const { bodyLimit } = this.getInstance().initialConfig;
581
- this.useBodyParser(contentType, withRawBody, { bodyLimit }, (req, body, done) => {
582
- const { onProtoPoisoning, onConstructorPoisoning } = this.instance.initialConfig;
583
- const defaultJsonParser = this.instance.getDefaultJsonParser(onProtoPoisoning || 'error', onConstructorPoisoning || 'error');
584
- defaultJsonParser(req, body, done);
585
- });
686
+ this.useBodyParser(contentType, withRawBody, { bodyLimit });
586
687
  }
587
688
  registerUrlencodedContentParser(rawBody) {
588
689
  const contentType = 'application/x-www-form-urlencoded';
690
+ if (this.hasCustomBodyParser(contentType)) {
691
+ return;
692
+ }
589
693
  const withRawBody = !!rawBody;
590
694
  const { bodyLimit } = this.getInstance().initialConfig;
591
- this.useBodyParser(contentType, withRawBody, { bodyLimit }, (_req, body, done) => {
592
- done(null, querystringParse(body.toString()));
593
- });
695
+ this.useBodyParser(contentType, withRawBody, { bodyLimit });
696
+ }
697
+ // A parser registered through `useBodyParser()` takes precedence over the
698
+ // default one, including a RegExp or catch-all ('*') parser matching the type.
699
+ hasCustomBodyParser(contentType) {
700
+ for (const type of this.registeredContentTypes) {
701
+ if (type === '*' || type === contentType) {
702
+ return true;
703
+ }
704
+ if (isString(type)) {
705
+ continue;
706
+ }
707
+ type.lastIndex = 0;
708
+ if (type.test(contentType)) {
709
+ return true;
710
+ }
711
+ }
712
+ return false;
713
+ }
714
+ // Fastify stores string content types trimmed and lower-cased.
715
+ normalizeContentType(type) {
716
+ return isString(type) ? type.trim().toLowerCase() : type;
717
+ }
718
+ getDefaultBodyParser(type) {
719
+ switch (this.normalizeContentType(type)) {
720
+ case 'application/json':
721
+ return (req, body, done) => {
722
+ const { onProtoPoisoning, onConstructorPoisoning } = this.instance.initialConfig;
723
+ const defaultJsonParser = this.instance.getDefaultJsonParser(onProtoPoisoning || 'error', onConstructorPoisoning || 'error');
724
+ defaultJsonParser(req, body, done);
725
+ };
726
+ case 'application/x-www-form-urlencoded':
727
+ return (_req, body, done) => {
728
+ done(null, querystringParse(body.toString()));
729
+ };
730
+ }
594
731
  }
595
732
  async registerMiddie() {
596
733
  this.isMiddieRegistered = true;
@@ -612,7 +749,7 @@ export class FastifyAdapter extends AbstractHttpAdapter {
612
749
  const routeToInject = {
613
750
  method: routerMethodKey,
614
751
  url: args[0],
615
- handler: handlerRef,
752
+ handler: this.requireNonEmptyParams(args[0], handlerRef),
616
753
  };
617
754
  if (!this.instance.supportedMethods.includes(routerMethodKey)) {
618
755
  this.instance.addHttpMethod(routerMethodKey, { hasBody: true });
@@ -643,6 +780,35 @@ export class FastifyAdapter extends AbstractHttpAdapter {
643
780
  }
644
781
  return this.instance.route(routeToInject);
645
782
  }
783
+ /**
784
+ * "find-my-way" matches an empty path segment for a route parameter
785
+ * ("/users//profile" -> { id: "" }), while "path-to-regexp", which
786
+ * middleware paths (and the ExpressAdapter's routes) are matched with,
787
+ * needs at least one character per parameter. Requests with an empty route
788
+ * parameter are passed to the not-found handler instead, as with the
789
+ * ExpressAdapter, so that middleware and routes always match the same
790
+ * requests. Only the wildcard ("*") may be empty.
791
+ */
792
+ requireNonEmptyParams(url, handler) {
793
+ if (isString(url) && !url.includes(':')) {
794
+ return handler;
795
+ }
796
+ const routeHandler = function (request, reply) {
797
+ const params = request.params;
798
+ for (const name in params) {
799
+ if (params[name] === '' && name !== '*') {
800
+ reply.callNotFound();
801
+ return;
802
+ }
803
+ }
804
+ return handler.call(this, request, reply);
805
+ };
806
+ Object.defineProperty(routeHandler, 'name', {
807
+ configurable: true,
808
+ value: handler.name,
809
+ });
810
+ return routeHandler;
811
+ }
646
812
  /**
647
813
  * Fastify still accepts the router options ("ignoreTrailingSlash",
648
814
  * "caseSensitive", ...) at the top level, but "initialConfig.routerOptions"
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Returns the version carried by an `Accept` header for media type versioning,
3
+ * e.g. `2` for `application/json;v=2` with the `v=` key.
4
+ *
5
+ * The version parameter may follow other parameters and may belong to any
6
+ * media range of a list (`text/html, application/json;v=2`); the first match
7
+ * wins. Returns `undefined` when no parameter starts with the key.
8
+ *
9
+ * Duplicated in each platform package so that a platform package never
10
+ * depends on a newer `@nestjs/core` than the one installed.
11
+ */
12
+ export declare function getMediaTypeVersion(acceptHeader: string | string[] | undefined, key: string): string | undefined;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Returns the version carried by an `Accept` header for media type versioning,
3
+ * e.g. `2` for `application/json;v=2` with the `v=` key.
4
+ *
5
+ * The version parameter may follow other parameters and may belong to any
6
+ * media range of a list (`text/html, application/json;v=2`); the first match
7
+ * wins. Returns `undefined` when no parameter starts with the key.
8
+ *
9
+ * Duplicated in each platform package so that a platform package never
10
+ * depends on a newer `@nestjs/core` than the one installed.
11
+ */
12
+ export function getMediaTypeVersion(acceptHeader, key) {
13
+ const mediaRanges = []
14
+ .concat(acceptHeader ?? [])
15
+ .flatMap(value => value.split(','));
16
+ for (const mediaRange of mediaRanges) {
17
+ const [, ...parameters] = mediaRange.split(';');
18
+ for (const parameter of parameters) {
19
+ const trimmedParameter = parameter.trim();
20
+ if (trimmedParameter.startsWith(key)) {
21
+ return trimmedParameter.slice(key.length);
22
+ }
23
+ }
24
+ }
25
+ return undefined;
26
+ }
@@ -39,6 +39,18 @@ export interface MultipartLimits {
39
39
  parts?: number;
40
40
  /** For multipart forms, the max number of header key => value pairs to parse (Default: 2000) */
41
41
  headerPairs?: number;
42
+ /**
43
+ * Max nesting depth of field names; `a[b][c]` has 2 levels (Default:
44
+ * Infinity). Like `fieldNameSize`, it does not fall back to the options
45
+ * `@fastify/multipart` was registered with.
46
+ */
47
+ fieldNestingDepth?: number;
48
+ /**
49
+ * Max numeric array index accepted in field names; `a[3]` uses index 3
50
+ * (Default: Infinity). Like `fieldNameSize`, it does not fall back to the
51
+ * options `@fastify/multipart` was registered with.
52
+ */
53
+ fieldArrayIndexLimit?: number;
42
54
  }
43
55
  /**
44
56
  * Same keys and semantics as platform-express's `MulterOptions`, so option
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Appends a text field to `store` using multer's bracket notation.
3
- * Returns `false` (leaving `store` untouched) for names that would pollute a
4
- * prototype.
3
+ * Returns `false` for names it cannot apply: names that would pollute a
4
+ * prototype (leaving `store` untouched) and appends to an array that already
5
+ * has the maximum length.
5
6
  */
6
7
  export declare function appendField(store: object, key: string, value: unknown): boolean;
@@ -84,11 +84,13 @@ function setValue(ctx, step, current, value) {
84
84
  }
85
85
  obj = Object.create(null);
86
86
  ctx[step.key] = obj;
87
- current.forEach((item, i) => {
88
- if (item !== undefined) {
89
- obj['' + i] = item;
87
+ // Only the indices that hold a value, so that the work depends on the
88
+ // number of items rather than on the length of a sparse array.
89
+ for (const index of Object.keys(current)) {
90
+ if (current[index] !== undefined) {
91
+ obj[index] = current[index];
90
92
  }
91
- });
93
+ }
92
94
  return obj;
93
95
  case 'scalar':
94
96
  obj = Object.create(null);
@@ -99,14 +101,23 @@ function setValue(ctx, step, current, value) {
99
101
  }
100
102
  /**
101
103
  * Appends a text field to `store` using multer's bracket notation.
102
- * Returns `false` (leaving `store` untouched) for names that would pollute a
103
- * prototype.
104
+ * Returns `false` for names it cannot apply: names that would pollute a
105
+ * prototype (leaving `store` untouched) and appends to an array that already
106
+ * has the maximum length.
104
107
  */
105
108
  export function appendField(store, key, value) {
106
109
  const steps = parsePath(key);
107
110
  if (steps.some(step => FORBIDDEN_KEYS.has(String(step.key)))) {
108
111
  return false;
109
112
  }
110
- steps.reduce((ctx, step) => setValue(ctx, step, ctx[step.key], value), store);
113
+ try {
114
+ steps.reduce((ctx, step) => setValue(ctx, step, ctx[step.key], value), store);
115
+ }
116
+ catch (err) {
117
+ if (err instanceof RangeError) {
118
+ return false;
119
+ }
120
+ throw err;
121
+ }
111
122
  return true;
112
123
  }
@@ -14,6 +14,7 @@ export declare const multerExceptions: {
14
14
  readonly LIMIT_UNEXPECTED_FILE: "Unexpected file field";
15
15
  readonly MISSING_FIELD_NAME: "Field name missing";
16
16
  readonly LIMIT_FIELD_NESTING: "Field name nesting too deep";
17
+ readonly LIMIT_FIELD_ARRAY_INDEX: "Field name array index too large";
17
18
  readonly INVALID_FIELD_NAME: "Invalid field name";
18
19
  };
19
20
  export type MultipartErrorCode = keyof typeof multerExceptions;
@@ -15,6 +15,7 @@ export const multerExceptions = {
15
15
  LIMIT_UNEXPECTED_FILE: 'Unexpected file field',
16
16
  MISSING_FIELD_NAME: 'Field name missing',
17
17
  LIMIT_FIELD_NESTING: 'Field name nesting too deep',
18
+ LIMIT_FIELD_ARRAY_INDEX: 'Field name array index too large',
18
19
  // A multer 2.4 code that platform-express's table lacks. Raised here for
19
20
  // field names that would pollute a prototype, which multer accepts.
20
21
  INVALID_FIELD_NAME: 'Invalid field name',
@@ -197,6 +197,36 @@ function checkFieldNameSize(rawName, limits) {
197
197
  throw new MultipartError('LIMIT_FIELD_KEY');
198
198
  }
199
199
  }
200
+ /**
201
+ * Whether the field name holds a numeric index above `limit`, decided as
202
+ * multer does. Only a name `appendField` parses as a bracket path builds an
203
+ * array; one it keeps as a literal key (`a[6]suffix`, `[6]`) never does.
204
+ */
205
+ function exceedsArrayIndexLimit(name, limit) {
206
+ if (!/^[^[]+(?:\[[^\]]+\])*(?:\[\])?$/.test(name)) {
207
+ return false;
208
+ }
209
+ for (const [, index] of name.matchAll(/\[(\d+)\]/g)) {
210
+ if (Number(index) > limit) {
211
+ return true;
212
+ }
213
+ }
214
+ return false;
215
+ }
216
+ /**
217
+ * `fieldNestingDepth` and `fieldArrayIndexLimit` are multer's own limits
218
+ * (busboy knows neither), so they are checked here, against the decoded name.
219
+ */
220
+ function checkFieldNameShape(name, limits) {
221
+ if (typeof limits?.fieldNestingDepth === 'number' &&
222
+ name.split('[').length - 1 > limits.fieldNestingDepth) {
223
+ throw new MultipartError('LIMIT_FIELD_NESTING', name);
224
+ }
225
+ if (typeof limits?.fieldArrayIndexLimit === 'number' &&
226
+ exceedsArrayIndexLimit(name, limits.fieldArrayIndexLimit)) {
227
+ throw new MultipartError('LIMIT_FIELD_ARRAY_INDEX', name);
228
+ }
229
+ }
200
230
  /**
201
231
  * Adds a text field to `body`, with multer's checks, in multer's order.
202
232
  */
@@ -209,6 +239,7 @@ function appendTextField(body, part, limits) {
209
239
  throw new MultipartError('LIMIT_FIELD_VALUE', part.fieldname);
210
240
  }
211
241
  checkFieldNameSize(rawName, limits);
242
+ checkFieldNameShape(part.fieldname, limits);
212
243
  if (!appendField(body, part.fieldname, part.value)) {
213
244
  throw new MultipartError('INVALID_FIELD_NAME', part.fieldname);
214
245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/platform-fastify",
3
- "version": "12.1.1",
3
+ "version": "12.1.3",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@platform-fastify)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -26,9 +26,9 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@fastify/cors": "11.3.0",
30
- "@fastify/formbody": "9.0.0",
31
- "@fastify/middie": "9.3.4",
29
+ "@fastify/cors": "11.3.1",
30
+ "@fastify/formbody": "9.0.1",
31
+ "@fastify/middie": "9.3.5",
32
32
  "fast-querystring": "1.1.2",
33
33
  "fastify": "5.12.5",
34
34
  "find-my-way": "9.9.0",
@@ -54,5 +54,5 @@
54
54
  "optional": true
55
55
  }
56
56
  },
57
- "gitHead": "9feedffde8ef8c86f0cd521153740575dc579d88"
57
+ "gitHead": "4514623279f6087808e3c8f08fbf9f37d3c17482"
58
58
  }