@nestjs/platform-fastify 12.1.2 → 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"
@@ -37,7 +37,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
37
37
  _pathPrefix;
38
38
  openConnections = new Set();
39
39
  isClosing = false;
40
+ isShuttingDown = false;
41
+ return503OnClosing = false;
40
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();
41
46
  onRequestHook;
42
47
  onResponseHook;
43
48
  isMiddieRegistered;
@@ -122,6 +127,9 @@ export class FastifyAdapter extends AbstractHttpAdapter {
122
127
  ...this.getTopLevelRouterOptions(instanceOrOptions),
123
128
  ...instanceOrOptions?.routerOptions,
124
129
  constraints: {
130
+ ...instanceOrOptions?.constraints,
131
+ ...instanceOrOptions?.routerOptions
132
+ ?.constraints,
125
133
  version: this.versionConstraint,
126
134
  },
127
135
  },
@@ -141,7 +149,28 @@ export class FastifyAdapter extends AbstractHttpAdapter {
141
149
  }
142
150
  this.instance.addHook('onRequest', (request, reply, done) => {
143
151
  if (this.onRequestHook) {
144
- 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
+ });
145
174
  }
146
175
  else {
147
176
  done();
@@ -149,13 +178,33 @@ export class FastifyAdapter extends AbstractHttpAdapter {
149
178
  });
150
179
  this.instance.addHook('onResponse', (request, reply, done) => {
151
180
  if (this.onResponseHook) {
152
- this.onResponseHook(request, reply, done);
181
+ this.runLifecycleHook(this.onResponseHook, request, reply, done);
153
182
  }
154
183
  else {
155
184
  done();
156
185
  }
157
186
  });
158
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
+ }
159
208
  setOnRequestHook(hook) {
160
209
  this.onRequestHook = hook;
161
210
  }
@@ -181,6 +230,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
181
230
  listen(listenOptions, ...args) {
182
231
  const isFirstArgTypeofFunction = typeof args[0] === 'function';
183
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
+ }
184
238
  let options;
185
239
  if (typeof listenOptions === 'object' &&
186
240
  (listenOptions.host !== undefined ||
@@ -197,8 +251,23 @@ export class FastifyAdapter extends AbstractHttpAdapter {
197
251
  if (!isFirstArgTypeofFunction) {
198
252
  options.host = args[0];
199
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
+ }
200
262
  return this.instance.listen(options, callback);
201
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
+ }
202
271
  get(...args) {
203
272
  return this.injectRouteOptions('GET', ...args);
204
273
  }
@@ -247,6 +316,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
247
316
  unlock(...args) {
248
317
  return this.injectRouteOptions('UNLOCK', ...args);
249
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
+ }
250
324
  applyVersionFilter(handler, version, versioningOptions) {
251
325
  if (!this.versioningOptions) {
252
326
  this.versioningOptions = versioningOptions;
@@ -303,7 +377,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
303
377
  return response.code(statusCode);
304
378
  }
305
379
  end(response, message) {
306
- 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);
307
386
  }
308
387
  render(response, view, options) {
309
388
  return response && response.view(view, options);
@@ -379,7 +458,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
379
458
  inject(opts) {
380
459
  return this.instance.inject(opts);
381
460
  }
461
+ beforeClose() {
462
+ this.isShuttingDown = true;
463
+ }
382
464
  async close() {
465
+ this.isShuttingDown = true;
383
466
  this.isClosing = true;
384
467
  try {
385
468
  this.closeOpenConnections();
@@ -395,6 +478,7 @@ export class FastifyAdapter extends AbstractHttpAdapter {
395
478
  }
396
479
  initHttpServer(options = {}) {
397
480
  this.httpServer = this.instance.server;
481
+ this.return503OnClosing = !!options?.return503OnClosing;
398
482
  if (options?.forceCloseConnections) {
399
483
  this.trackOpenConnections();
400
484
  }
@@ -415,7 +499,13 @@ export class FastifyAdapter extends AbstractHttpAdapter {
415
499
  return this.register(loadPackage('@fastify/view', 'FastifyAdapter.setViewEngine()', () => import('@fastify/view')), options);
416
500
  }
417
501
  isHeadersSent(response) {
418
- 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;
419
509
  }
420
510
  getHeader(response, name) {
421
511
  return response.getHeader(name);
@@ -478,23 +568,29 @@ export class FastifyAdapter extends AbstractHttpAdapter {
478
568
  : undefined;
479
569
  }
480
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
+ }
481
578
  const parserOptions = {
482
579
  ...(options || {}),
483
580
  parseAs: 'buffer',
484
581
  };
582
+ const bodyParser = parser ?? this.getDefaultBodyParser(type);
485
583
  this.getInstance().addContentTypeParser(type, parserOptions, (req, body, done) => {
486
584
  if (rawBody === true && Buffer.isBuffer(body)) {
487
585
  req.rawBody = body;
488
586
  }
489
- if (parser) {
490
- parser(req, body, done);
587
+ if (bodyParser) {
588
+ bodyParser(req, body, done);
491
589
  return;
492
590
  }
493
591
  done(null, body);
494
592
  });
495
- // To avoid the Nest application init to override our custom
496
- // body parser, we mark the parsers as registered.
497
- this._isParserRegistered = true;
593
+ this.registeredContentTypes.add(this.normalizeContentType(type));
498
594
  }
499
595
  async createMiddlewareFactory(requestMethod) {
500
596
  if (!this.isMiddieRegistered) {
@@ -582,21 +678,56 @@ export class FastifyAdapter extends AbstractHttpAdapter {
582
678
  }
583
679
  registerJsonContentParser(rawBody) {
584
680
  const contentType = 'application/json';
681
+ if (this.hasCustomBodyParser(contentType)) {
682
+ return;
683
+ }
585
684
  const withRawBody = !!rawBody;
586
685
  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
- });
686
+ this.useBodyParser(contentType, withRawBody, { bodyLimit });
592
687
  }
593
688
  registerUrlencodedContentParser(rawBody) {
594
689
  const contentType = 'application/x-www-form-urlencoded';
690
+ if (this.hasCustomBodyParser(contentType)) {
691
+ return;
692
+ }
595
693
  const withRawBody = !!rawBody;
596
694
  const { bodyLimit } = this.getInstance().initialConfig;
597
- this.useBodyParser(contentType, withRawBody, { bodyLimit }, (_req, body, done) => {
598
- done(null, querystringParse(body.toString()));
599
- });
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
+ }
600
731
  }
601
732
  async registerMiddie() {
602
733
  this.isMiddieRegistered = true;
@@ -618,7 +749,7 @@ export class FastifyAdapter extends AbstractHttpAdapter {
618
749
  const routeToInject = {
619
750
  method: routerMethodKey,
620
751
  url: args[0],
621
- handler: handlerRef,
752
+ handler: this.requireNonEmptyParams(args[0], handlerRef),
622
753
  };
623
754
  if (!this.instance.supportedMethods.includes(routerMethodKey)) {
624
755
  this.instance.addHttpMethod(routerMethodKey, { hasBody: true });
@@ -649,6 +780,35 @@ export class FastifyAdapter extends AbstractHttpAdapter {
649
780
  }
650
781
  return this.instance.route(routeToInject);
651
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
+ }
652
812
  /**
653
813
  * Fastify still accepts the router options ("ignoreTrailingSlash",
654
814
  * "caseSensitive", ...) at the top level, but "initialConfig.routerOptions"
@@ -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.2",
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": "8ab60aa1b2927fbe0e23135ffc65d610c623236a"
57
+ "gitHead": "4514623279f6087808e3c8f08fbf9f37d3c17482"
58
58
  }