@nestjs/platform-express 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.
@@ -5,7 +5,7 @@ import * as http from 'http';
5
5
  import * as https from 'https';
6
6
  import { NestExpressBodyParserOptionsFor, NestExpressBodyParserType } from '../interfaces/nest-express-body-parser.interface.js';
7
7
  import { ServeStaticOptions } from '../interfaces/serve-static-options.interface.js';
8
- import { type CorsOptions, type CorsOptionsDelegate, type VersionValue } from '@nestjs/common/internal';
8
+ import { type CorsOptions, type CorsOptionsDelegate, type RouteInfo, type VersionValue } from '@nestjs/common/internal';
9
9
  import type { NestApplicationOptions } from '@nestjs/common';
10
10
  import { AbstractHttpAdapter } from '@nestjs/core';
11
11
  type VersionedRoute = <TRequest extends Record<string, any> = any, TResponse = any>(req: TRequest, res: TResponse, next: () => void) => any;
@@ -21,7 +21,18 @@ export declare class ExpressAdapter extends AbstractHttpAdapter<http.Server | ht
21
21
  private onRequestHook?;
22
22
  private onResponseHook?;
23
23
  constructor(instance?: any);
24
- setOnRequestHook(onRequestHook: (req: express.Request, res: express.Response, done: () => void) => Promise<void> | void): void;
24
+ private runRequestHook;
25
+ /**
26
+ * Registers a request hook. Call `done()` to continue processing the request;
27
+ * resolving the returned promise does not continue it. Only the first
28
+ * completion is forwarded to Express. Errors after `done()` are logged
29
+ * without advancing the request again.
30
+ */
31
+ setOnRequestHook(onRequestHook: (req: express.Request, res: express.Response, done: (err?: Error) => void) => Promise<void> | void): void;
32
+ /**
33
+ * Registers a hook to run after the response has been sent.
34
+ * Thrown errors and rejected promises are logged.
35
+ */
25
36
  setOnResponseHook(onResponseHook: (req: express.Request, res: express.Response) => Promise<void> | void): void;
26
37
  reply(response: any, body: any, statusCode?: number): any;
27
38
  status(response: any, statusCode: number): any;
@@ -29,7 +40,7 @@ export declare class ExpressAdapter extends AbstractHttpAdapter<http.Server | ht
29
40
  render(response: any, view: string, options: any): any;
30
41
  redirect(response: any, statusCode: number, url: string): any;
31
42
  setErrorHandler(handler: Function, prefix?: string): any;
32
- setNotFoundHandler(handler: Function, prefix?: string): any;
43
+ setNotFoundHandler(handler: Function, prefix?: string, excludedRoutes?: RouteInfo[]): any;
33
44
  isHeadersSent(response: any): boolean;
34
45
  getHeader(response: any, name: string): any;
35
46
  setHeader(response: any, name: string, value: string): any;
@@ -57,6 +68,10 @@ export declare class ExpressAdapter extends AbstractHttpAdapter<http.Server | ht
57
68
  setLocal(key: string, value: any): this;
58
69
  getType(): string;
59
70
  isRouteOrderSensitive(): boolean;
71
+ getRouteMatchingOptions(): {
72
+ caseSensitive: boolean;
73
+ ignoreTrailingSlash: boolean;
74
+ };
60
75
  applyVersionFilter(handler: Function, version: VersionValue, versioningOptions: VersioningOptions): VersionedRoute;
61
76
  mapException(error: unknown): unknown;
62
77
  private normalizePrefix;
@@ -4,6 +4,7 @@ import express from 'express';
4
4
  import * as http from 'http';
5
5
  import * as https from 'https';
6
6
  import { pathToRegexp } from 'path-to-regexp';
7
+ import { finished } from 'stream';
7
8
  import { getBodyParserOptions } from './utils/get-body-parser-options.util.js';
8
9
  import { getMediaTypeVersion } from './utils/get-media-type-version.util.js';
9
10
  import { addLeadingSlash, isFunction, isNil, isObject, isString, isUndefined, stripEndSlash, } from '@nestjs/common/internal';
@@ -30,21 +31,59 @@ export class ExpressAdapter extends AbstractHttpAdapter {
30
31
  super(instance || express());
31
32
  this.instance.use((req, res, next) => {
32
33
  if (this.onResponseHook) {
33
- res.on('finish', () => {
34
- void this.onResponseHook.apply(this, [req, res]);
34
+ res.on('finish', async () => {
35
+ try {
36
+ await this.onResponseHook.apply(this, [req, res]);
37
+ }
38
+ catch (err) {
39
+ this.logger.error(err);
40
+ }
35
41
  });
36
42
  }
37
43
  if (this.onRequestHook) {
38
- void this.onRequestHook.apply(this, [req, res, next]);
44
+ this.runRequestHook(req, res, next);
39
45
  }
40
46
  else {
41
47
  next();
42
48
  }
43
49
  });
44
50
  }
51
+ runRequestHook(req, res, next) {
52
+ let completed = false;
53
+ const doneOnce = (err) => {
54
+ if (completed) {
55
+ if (err) {
56
+ this.logger.error(err);
57
+ }
58
+ return;
59
+ }
60
+ completed = true;
61
+ next(err);
62
+ };
63
+ try {
64
+ // Not returned to Express: rejections share the once-guard with done()
65
+ const result = this.onRequestHook.call(this, req, res, doneOnce);
66
+ if (result && typeof result.then === 'function') {
67
+ result.then(undefined, err => doneOnce(err || new Error('Request hook failed')));
68
+ }
69
+ }
70
+ catch (err) {
71
+ doneOnce(err || new Error('Request hook failed'));
72
+ }
73
+ }
74
+ /**
75
+ * Registers a request hook. Call `done()` to continue processing the request;
76
+ * resolving the returned promise does not continue it. Only the first
77
+ * completion is forwarded to Express. Errors after `done()` are logged
78
+ * without advancing the request again.
79
+ */
45
80
  setOnRequestHook(onRequestHook) {
46
81
  this.onRequestHook = onRequestHook;
47
82
  }
83
+ /**
84
+ * Registers a hook to run after the response has been sent.
85
+ * Thrown errors and rejected promises are logged.
86
+ */
48
87
  setOnResponseHook(onResponseHook) {
49
88
  this.onResponseHook = onResponseHook;
50
89
  }
@@ -56,11 +95,32 @@ export class ExpressAdapter extends AbstractHttpAdapter {
56
95
  return response.send();
57
96
  }
58
97
  if (body instanceof StreamableFile) {
98
+ // A stream that failed before the reply (e.g. while an interceptor
99
+ // awaited) has already emitted its 'error' event.
100
+ const { errored } = body.getStream();
101
+ if (errored) {
102
+ return body.errorHandler(errored, response);
103
+ }
59
104
  this.applyStreamHeaders(response, body);
60
105
  const stream = body.getStream();
61
106
  stream.once('error', err => {
62
107
  body.errorHandler(err, response);
63
108
  });
109
+ // pipe() leaves the source open when the client disconnects early,
110
+ // also before reply() runs ("close" has already fired by then)
111
+ finished(response, () => {
112
+ if (!stream.readableEnded) {
113
+ stream.destroy();
114
+ }
115
+ });
116
+ // Node discards the body of a HEAD response without ever applying
117
+ // backpressure, so piping would read the whole source. The response
118
+ // ends once the source is readable instead, which still leaves an
119
+ // error opening it (e.g. a missing file) to the error handler.
120
+ if (response.req?.method === 'HEAD') {
121
+ stream.once('readable', () => response.end());
122
+ return response.on('error', (err) => body.errorLogger(err));
123
+ }
64
124
  return stream
65
125
  .pipe(response)
66
126
  .on('error', (err) => body.errorLogger(err));
@@ -68,7 +128,12 @@ export class ExpressAdapter extends AbstractHttpAdapter {
68
128
  const responseContentType = response.getHeader('Content-Type');
69
129
  if (typeof responseContentType === 'string' &&
70
130
  !isJsonContentType(responseContentType) &&
71
- body?.statusCode >= HttpStatus.BAD_REQUEST) {
131
+ (body?.statusCode >= HttpStatus.BAD_REQUEST ||
132
+ // A custom error body (e.g. "new HttpException({ ... }, 404)") need
133
+ // not have a "statusCode" of its own
134
+ (isObject(body) &&
135
+ !isNil(statusCode) &&
136
+ statusCode >= HttpStatus.BAD_REQUEST))) {
72
137
  this.logger.warn("Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses");
73
138
  response.setHeader('Content-Type', 'application/json');
74
139
  }
@@ -98,13 +163,23 @@ export class ExpressAdapter extends AbstractHttpAdapter {
98
163
  // root-mounted routes) still pass through the exception layer.
99
164
  return this.use(handler);
100
165
  }
101
- setNotFoundHandler(handler, prefix) {
166
+ setNotFoundHandler(handler, prefix, excludedRoutes) {
102
167
  const normalizedPrefix = this.normalizePrefix(prefix);
103
168
  if (normalizedPrefix) {
104
169
  this.registeredPrefixes.add(normalizedPrefix);
105
170
  const router = express.Router();
106
171
  router.all('*path', handler);
107
- return this.use(normalizedPrefix, router);
172
+ this.use(normalizedPrefix, router);
173
+ // Excluded routes live at the root, out of the prefix router's reach.
174
+ // Mount the handler on each exact path, for every method, so a method
175
+ // miss there (`POST /hello` with only `GET /hello`) still reaches the
176
+ // exception layer. Not `use(path, router)`: a subtree mount would claim
177
+ // a raw `/hello/live` added after `init()`; a wildcard exclusion covers
178
+ // its subtree through its own pattern.
179
+ for (const route of excludedRoutes ?? []) {
180
+ this.instance.all(addLeadingSlash(route.path), handler);
181
+ }
182
+ return;
108
183
  }
109
184
  return this.use((req, res, next) => {
110
185
  // When multiple apps share this adapter, a non-prefixed app's 404
@@ -202,10 +277,20 @@ export class ExpressAdapter extends AbstractHttpAdapter {
202
277
  createMiddlewareFactory(requestMethod) {
203
278
  return (path, callback) => {
204
279
  try {
205
- const convertedPath = LegacyRouteConverter.tryConvert(path);
206
- return this.routerMethodFactory
207
- .get(this.instance, requestMethod)
208
- .call(this.instance, convertedPath, callback);
280
+ // The core marks an exact-match path with a trailing "$" (e.g. "/api$").
281
+ // Express 5 reads "$" literally and "use()" matches by prefix, so strip
282
+ // the marker and register the path as an exact "all()" route instead.
283
+ const isExactPath = path.endsWith('$');
284
+ const convertedPath = LegacyRouteConverter.tryConvert(isExactPath ? path.slice(0, -1) : path);
285
+ let router = this.routerMethodFactory.get(this.instance, requestMethod);
286
+ if (isExactPath && router === this.instance.use) {
287
+ router = this.instance.all;
288
+ }
289
+ // Express routes are not strict, so "/api" also matches "/api/", which
290
+ // the wildcard path registered next to it may match too ("/api/{*path}"
291
+ // does, "/api/*path" does not). The core runs the middleware for the
292
+ // first of its paths that matches a request only.
293
+ return router.call(this.instance, convertedPath, callback);
209
294
  }
210
295
  catch (e) {
211
296
  if (e instanceof TypeError) {
@@ -265,6 +350,18 @@ export class ExpressAdapter extends AbstractHttpAdapter {
265
350
  isRouteOrderSensitive() {
266
351
  return true;
267
352
  }
353
+ // The return type is spelled out (rather than "RouteMatchingOptions") so
354
+ // that the declarations still compile against an older "@nestjs/common"
355
+ getRouteMatchingOptions() {
356
+ // Routes are registered on the application router, which takes the
357
+ // "case sensitive routing" and "strict routing" settings in effect when
358
+ // it is created and ignores later changes
359
+ const router = this.getInstance().router;
360
+ return {
361
+ caseSensitive: !!router?.caseSensitive,
362
+ ignoreTrailingSlash: !router?.strict,
363
+ };
364
+ }
268
365
  applyVersionFilter(handler, version, versioningOptions) {
269
366
  const callNextHandler = (req, res, next) => {
270
367
  if (!next) {
@@ -6,6 +6,10 @@
6
6
  * media range of a list (`text/html, application/json;v=2`); the first match
7
7
  * wins. Returns `undefined` when no parameter starts with the key.
8
8
  *
9
+ * Quoted parameter values are unescaped, and separators inside them are ignored.
10
+ * An unterminated quoted string runs to the end of the header value, so a
11
+ * version that follows its opening quote is not read.
12
+ *
9
13
  * Duplicated in each platform package so that a platform package never
10
14
  * depends on a newer `@nestjs/core` than the one installed.
11
15
  */
@@ -1,3 +1,29 @@
1
+ /**
2
+ * Splits a header value at separators outside quoted strings in linear time.
3
+ * Backslash-escaped characters inside quoted strings are treated as literals.
4
+ */
5
+ const splitOutsideQuotes = (value, separator) => {
6
+ const parts = [];
7
+ let start = 0;
8
+ let isQuoted = false;
9
+ const len = value.length;
10
+ for (let index = 0; index < len; index++) {
11
+ const character = value[index];
12
+ if (isQuoted && character === '\\') {
13
+ // A quoted-pair consumes the next character, including an escaped quote.
14
+ index++;
15
+ }
16
+ else if (character === '"') {
17
+ isQuoted = !isQuoted;
18
+ }
19
+ else if (character === separator && !isQuoted) {
20
+ parts.push(value.slice(start, index));
21
+ start = index + 1;
22
+ }
23
+ }
24
+ parts.push(value.slice(start));
25
+ return parts;
26
+ };
1
27
  /**
2
28
  * Returns the version carried by an `Accept` header for media type versioning,
3
29
  * e.g. `2` for `application/json;v=2` with the `v=` key.
@@ -6,19 +32,27 @@
6
32
  * media range of a list (`text/html, application/json;v=2`); the first match
7
33
  * wins. Returns `undefined` when no parameter starts with the key.
8
34
  *
35
+ * Quoted parameter values are unescaped, and separators inside them are ignored.
36
+ * An unterminated quoted string runs to the end of the header value, so a
37
+ * version that follows its opening quote is not read.
38
+ *
9
39
  * Duplicated in each platform package so that a platform package never
10
40
  * depends on a newer `@nestjs/core` than the one installed.
11
41
  */
12
42
  export function getMediaTypeVersion(acceptHeader, key) {
13
43
  const mediaRanges = []
14
44
  .concat(acceptHeader ?? [])
15
- .flatMap(value => value.split(','));
45
+ .flatMap(value => splitOutsideQuotes(value, ','));
16
46
  for (const mediaRange of mediaRanges) {
17
- const [, ...parameters] = mediaRange.split(';');
47
+ const [, ...parameters] = splitOutsideQuotes(mediaRange, ';');
18
48
  for (const parameter of parameters) {
19
49
  const trimmedParameter = parameter.trim();
20
50
  if (trimmedParameter.startsWith(key)) {
21
- return trimmedParameter.slice(key.length);
51
+ const version = trimmedParameter.slice(key.length);
52
+ if (version.startsWith('"') && version.endsWith('"')) {
53
+ return version.slice(1, -1).replace(/\\([\s\S])/g, '$1');
54
+ }
55
+ return version;
22
56
  }
23
57
  }
24
58
  }
@@ -2,7 +2,7 @@ import { __decorate, __metadata, __param } from "tslib";
2
2
  import { Inject, mixin, Optional, } from '@nestjs/common';
3
3
  import multer from 'multer';
4
4
  import { MULTER_MODULE_OPTIONS } from '../files.constants.js';
5
- import { mergeMulterOptions, transformException, } from '../multer/multer.utils.js';
5
+ import { applyDefaultLimits, mergeMulterOptions, removeFilesOnPipeRejection, transformException, } from '../multer/multer.utils.js';
6
6
  /**
7
7
  * @param localOptions
8
8
  *
@@ -12,7 +12,7 @@ export function AnyFilesInterceptor(localOptions) {
12
12
  let MixinInterceptor = class MixinInterceptor {
13
13
  multer;
14
14
  constructor(options = {}) {
15
- this.multer = multer(mergeMulterOptions(options, localOptions));
15
+ this.multer = multer(applyDefaultLimits(mergeMulterOptions(options, localOptions)));
16
16
  }
17
17
  async intercept(context, next) {
18
18
  const ctx = context.switchToHttp();
@@ -23,7 +23,9 @@ export function AnyFilesInterceptor(localOptions) {
23
23
  }
24
24
  resolve();
25
25
  }));
26
- return next.handle();
26
+ return next
27
+ .handle()
28
+ .pipe(removeFilesOnPipeRejection(this.multer.storage, ctx.getRequest(), ctx.getResponse()));
27
29
  }
28
30
  };
29
31
  MixinInterceptor = __decorate([
@@ -2,7 +2,7 @@ import { __decorate, __metadata, __param } from "tslib";
2
2
  import { Inject, mixin, Optional, } from '@nestjs/common';
3
3
  import multer from 'multer';
4
4
  import { MULTER_MODULE_OPTIONS } from '../files.constants.js';
5
- import { mergeMulterOptions, transformException, } from '../multer/multer.utils.js';
5
+ import { applyDefaultLimits, mergeMulterOptions, removeFilesOnPipeRejection, transformException, } from '../multer/multer.utils.js';
6
6
  /**
7
7
  * @param uploadFields
8
8
  * @param localOptions
@@ -12,7 +12,7 @@ export function FileFieldsInterceptor(uploadFields, localOptions) {
12
12
  let MixinInterceptor = class MixinInterceptor {
13
13
  multer;
14
14
  constructor(options = {}) {
15
- this.multer = multer(mergeMulterOptions(options, localOptions));
15
+ this.multer = multer(applyDefaultLimits(mergeMulterOptions(options, localOptions)));
16
16
  }
17
17
  async intercept(context, next) {
18
18
  const ctx = context.switchToHttp();
@@ -23,7 +23,9 @@ export function FileFieldsInterceptor(uploadFields, localOptions) {
23
23
  }
24
24
  resolve();
25
25
  }));
26
- return next.handle();
26
+ return next
27
+ .handle()
28
+ .pipe(removeFilesOnPipeRejection(this.multer.storage, ctx.getRequest(), ctx.getResponse()));
27
29
  }
28
30
  };
29
31
  MixinInterceptor = __decorate([
@@ -2,7 +2,7 @@ import { __decorate, __metadata, __param } from "tslib";
2
2
  import { Inject, mixin, Optional, } from '@nestjs/common';
3
3
  import multer from 'multer';
4
4
  import { MULTER_MODULE_OPTIONS } from '../files.constants.js';
5
- import { mergeMulterOptions, transformException, } from '../multer/multer.utils.js';
5
+ import { applyDefaultLimits, mergeMulterOptions, removeFilesOnPipeRejection, transformException, } from '../multer/multer.utils.js';
6
6
  /**
7
7
  * @param fieldName
8
8
  * @param localOptions
@@ -13,7 +13,7 @@ export function FileInterceptor(fieldName, localOptions) {
13
13
  let MixinInterceptor = class MixinInterceptor {
14
14
  multer;
15
15
  constructor(options = {}) {
16
- this.multer = multer(mergeMulterOptions(options, localOptions));
16
+ this.multer = multer(applyDefaultLimits(mergeMulterOptions(options, localOptions)));
17
17
  }
18
18
  async intercept(context, next) {
19
19
  const ctx = context.switchToHttp();
@@ -24,7 +24,9 @@ export function FileInterceptor(fieldName, localOptions) {
24
24
  }
25
25
  resolve();
26
26
  }));
27
- return next.handle();
27
+ return next
28
+ .handle()
29
+ .pipe(removeFilesOnPipeRejection(this.multer.storage, ctx.getRequest(), ctx.getResponse()));
28
30
  }
29
31
  };
30
32
  MixinInterceptor = __decorate([
@@ -2,7 +2,7 @@ import { __decorate, __metadata, __param } from "tslib";
2
2
  import { Inject, mixin, Optional, } from '@nestjs/common';
3
3
  import multer from 'multer';
4
4
  import { MULTER_MODULE_OPTIONS } from '../files.constants.js';
5
- import { mergeMulterOptions, transformException, } from '../multer/multer.utils.js';
5
+ import { applyDefaultLimits, mergeMulterOptions, removeFilesOnPipeRejection, transformException, } from '../multer/multer.utils.js';
6
6
  /**
7
7
  *
8
8
  * @param fieldName
@@ -15,7 +15,7 @@ export function FilesInterceptor(fieldName, maxCount, localOptions) {
15
15
  let MixinInterceptor = class MixinInterceptor {
16
16
  multer;
17
17
  constructor(options = {}) {
18
- this.multer = multer(mergeMulterOptions(options, localOptions));
18
+ this.multer = multer(applyDefaultLimits(mergeMulterOptions(options, localOptions)));
19
19
  }
20
20
  async intercept(context, next) {
21
21
  const ctx = context.switchToHttp();
@@ -26,7 +26,9 @@ export function FilesInterceptor(fieldName, maxCount, localOptions) {
26
26
  }
27
27
  resolve();
28
28
  }));
29
- return next.handle();
29
+ return next
30
+ .handle()
31
+ .pipe(removeFilesOnPipeRejection(this.multer.storage, ctx.getRequest(), ctx.getResponse()));
30
32
  }
31
33
  };
32
34
  MixinInterceptor = __decorate([
@@ -2,7 +2,7 @@ import { __decorate, __metadata, __param } from "tslib";
2
2
  import { Inject, mixin, Optional, } from '@nestjs/common';
3
3
  import multer from 'multer';
4
4
  import { MULTER_MODULE_OPTIONS } from '../files.constants.js';
5
- import { mergeMulterOptions, transformException, } from '../multer/multer.utils.js';
5
+ import { applyDefaultLimits, mergeMulterOptions, transformException, } from '../multer/multer.utils.js';
6
6
  /**
7
7
  *
8
8
  * @param localOptions
@@ -12,7 +12,7 @@ export function NoFilesInterceptor(localOptions) {
12
12
  let MixinInterceptor = class MixinInterceptor {
13
13
  multer;
14
14
  constructor(options = {}) {
15
- this.multer = multer(mergeMulterOptions(options, localOptions));
15
+ this.multer = multer(applyDefaultLimits(mergeMulterOptions(options, localOptions)));
16
16
  }
17
17
  async intercept(context, next) {
18
18
  const ctx = context.switchToHttp();
@@ -16,6 +16,18 @@ export interface MulterLimits {
16
16
  parts?: number;
17
17
  /** For multipart forms, the max number of header key=> value pairs to parse Default: 2000(same as node's http). */
18
18
  headerPairs?: number;
19
+ /**
20
+ * Max nesting depth of field names; `a[b][c]` has 2 levels (Default: 32,
21
+ * where multer's own default is Infinity). Set it to `Infinity` to lift
22
+ * the limit.
23
+ */
24
+ fieldNestingDepth?: number;
25
+ /**
26
+ * Max numeric array index accepted in field names; `a[3]` uses index 3
27
+ * (Default: 1000, where multer's own default is Infinity). Set it to
28
+ * `Infinity` to lift the limit.
29
+ */
30
+ fieldArrayIndexLimit?: number;
19
31
  }
20
32
  /**
21
33
  * @see https://github.com/expressjs/multer
@@ -27,8 +39,9 @@ export interface MulterOptions {
27
39
  /** The storage engine to use for uploaded files. */
28
40
  storage?: any;
29
41
  /**
30
- * An object specifying the size limits of the following optional properties. This object is passed to busboy
31
- * directly, and the details of properties can be found on https://github.com/mscdex/busboy#busboy-methods
42
+ * An object specifying the size limits of the following optional properties. This object is passed to multer
43
+ * and on to busboy, with `fieldNestingDepth` and `fieldArrayIndexLimit` set to their defaults when absent.
44
+ * The details of the busboy properties can be found on https://github.com/mscdex/busboy#busboy-methods
32
45
  *
33
46
  * Since multer 2.4.0, this can also be a function of the request that returns such an object.
34
47
  */
@@ -11,6 +11,17 @@ export declare const multerExceptions: {
11
11
  LIMIT_FIELD_ARRAY_INDEX: string;
12
12
  INVALID_FIELD_NAME: string;
13
13
  };
14
+ /**
15
+ * Defaults of the field name limits that multer leaves at `Infinity`, which
16
+ * let a single small request build an arbitrarily deep `req.body`
17
+ * (`fieldNestingDepth`) or arrays of arbitrary length (`fieldArrayIndexLimit`).
18
+ * `32` is the nesting depth `body-parser` allows in the urlencoded bodies the
19
+ * ExpressAdapter parses; `1000` is the number of parameters it parses.
20
+ */
21
+ export declare const DEFAULT_FIELD_NAME_LIMITS: {
22
+ readonly fieldNestingDepth: 32;
23
+ readonly fieldArrayIndexLimit: 1000;
24
+ };
14
25
  export declare const busboyExceptions: {
15
26
  MULTIPART_BOUNDARY_NOT_FOUND: string;
16
27
  MULTIPART_MALFORMED_PART_HEADER: string;
@@ -15,6 +15,17 @@ export const multerExceptions = {
15
15
  // engine after the file stream was destroyed, not by the request, so it
16
16
  // stays a 500.
17
17
  };
18
+ /**
19
+ * Defaults of the field name limits that multer leaves at `Infinity`, which
20
+ * let a single small request build an arbitrarily deep `req.body`
21
+ * (`fieldNestingDepth`) or arrays of arbitrary length (`fieldArrayIndexLimit`).
22
+ * `32` is the nesting depth `body-parser` allows in the urlencoded bodies the
23
+ * ExpressAdapter parses; `1000` is the number of parameters it parses.
24
+ */
25
+ export const DEFAULT_FIELD_NAME_LIMITS = {
26
+ fieldNestingDepth: 32,
27
+ fieldArrayIndexLimit: 1000,
28
+ };
18
29
  export const busboyExceptions = {
19
30
  // from https://github.com/mscdex/busboy/blob/master/lib/types/multipart.js
20
31
  MULTIPART_BOUNDARY_NOT_FOUND: 'Multipart: Boundary not found',
@@ -1,4 +1,5 @@
1
1
  import { HttpException } from '@nestjs/common';
2
+ import { type MonoTypeOperatorFunction } from 'rxjs';
2
3
  import { MulterModuleOptions } from '../interfaces/index.js';
3
4
  import { MulterOptions } from '../interfaces/multer-options.interface.js';
4
5
  /**
@@ -12,6 +13,20 @@ import { MulterOptions } from '../interfaces/multer-options.interface.js';
12
13
  * `limits`) the local value wins outright, matching the pre-merge behaviour.
13
14
  */
14
15
  export declare function mergeMulterOptions(options?: MulterModuleOptions, localOptions?: MulterOptions): MulterModuleOptions;
16
+ /**
17
+ * Sets the field name limits multer leaves at `Infinity` to
18
+ * `DEFAULT_FIELD_NAME_LIMITS` wherever they are not set. multer only checks
19
+ * a limit that is present in `limits`, so the defaults are added to the
20
+ * object, or to the object a function-valued `limits` returns.
21
+ */
22
+ export declare function applyDefaultLimits<T extends MulterOptions>(options: T): T;
23
+ /**
24
+ * Removes the files multer stored for `req` when the route's pipes reject
25
+ * the request (e.g. `ParseFilePipe`): the route handler never sees them, and
26
+ * nothing else would remove them. They are removed once the response has
27
+ * been sent, as an exception filter may still use (or remove) them.
28
+ */
29
+ export declare function removeFilesOnPipeRejection<T>(storage: any, req: any, res: any): MonoTypeOperatorFunction<T>;
15
30
  export declare function transformException(error: (Error & {
16
31
  field?: string;
17
32
  code?: string;
@@ -1,6 +1,9 @@
1
1
  import { BadRequestException, HttpException, PayloadTooLargeException, } from '@nestjs/common';
2
2
  import { isPlainObject } from '@nestjs/common/internal';
3
- import { multerExceptions, busboyExceptions } from './multer.constants.js';
3
+ import { isRejectedByPipes } from '@nestjs/core/internal';
4
+ import { catchError, throwError } from 'rxjs';
5
+ import { finished } from 'stream';
6
+ import { DEFAULT_FIELD_NAME_LIMITS, multerExceptions, busboyExceptions, } from './multer.constants.js';
4
7
  /**
5
8
  * Merges module-level (global) multer options with interceptor-level (local)
6
9
  * options. `limits` is merged key-by-key rather than replaced outright, so
@@ -16,8 +19,66 @@ export function mergeMulterOptions(options = {}, localOptions = {}) {
16
19
  if (isPlainObject(options.limits) && isPlainObject(localOptions.limits)) {
17
20
  merged.limits = { ...options.limits, ...localOptions.limits };
18
21
  }
22
+ // `dest` and `storage` both say where files go, and multer prefers
23
+ // `storage`, so a global storage engine would shadow a local `dest`.
24
+ if (localOptions.dest && !localOptions.storage) {
25
+ delete merged.storage;
26
+ }
19
27
  return merged;
20
28
  }
29
+ /**
30
+ * Sets the field name limits multer leaves at `Infinity` to
31
+ * `DEFAULT_FIELD_NAME_LIMITS` wherever they are not set. multer only checks
32
+ * a limit that is present in `limits`, so the defaults are added to the
33
+ * object, or to the object a function-valued `limits` returns.
34
+ */
35
+ export function applyDefaultLimits(options) {
36
+ const { limits } = options;
37
+ return {
38
+ ...options,
39
+ limits: typeof limits === 'function'
40
+ ? (req) => withDefaultLimits(limits(req))
41
+ : withDefaultLimits(limits),
42
+ };
43
+ }
44
+ function withDefaultLimits(limits) {
45
+ return {
46
+ ...limits,
47
+ fieldNestingDepth: limits?.fieldNestingDepth ?? DEFAULT_FIELD_NAME_LIMITS.fieldNestingDepth,
48
+ fieldArrayIndexLimit: limits?.fieldArrayIndexLimit ??
49
+ DEFAULT_FIELD_NAME_LIMITS.fieldArrayIndexLimit,
50
+ };
51
+ }
52
+ /**
53
+ * Removes the files multer stored for `req` when the route's pipes reject
54
+ * the request (e.g. `ParseFilePipe`): the route handler never sees them, and
55
+ * nothing else would remove them. They are removed once the response has
56
+ * been sent, as an exception filter may still use (or remove) them.
57
+ */
58
+ export function removeFilesOnPipeRejection(storage, req, res) {
59
+ return catchError(err => {
60
+ if (isRejectedByPipes(req)) {
61
+ finished(res, () => {
62
+ for (const file of storedFiles(req)) {
63
+ try {
64
+ storage._removeFile(req, file, () => undefined);
65
+ }
66
+ catch {
67
+ // The request has failed already.
68
+ }
69
+ }
70
+ });
71
+ }
72
+ return throwError(() => err);
73
+ });
74
+ }
75
+ function storedFiles(req) {
76
+ const { file, files } = req;
77
+ if (file) {
78
+ return [file];
79
+ }
80
+ return Array.isArray(files) ? files : Object.values(files ?? {}).flat();
81
+ }
21
82
  // Multer may add in a 'field' property to the error
22
83
  // https://github.com/expressjs/multer/blob/aa42bea6ac7d0cb8fcb279b15a7278cda805dc63/lib/multer-error.js#L19
23
84
  export function transformException(error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/platform-express",
3
- "version": "12.1.2",
3
+ "version": "12.2.0",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@platform-express)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -8,6 +8,7 @@
8
8
  "main": "./index.js",
9
9
  "exports": {
10
10
  ".": "./index.js",
11
+ "./package.json": "./package.json",
11
12
  "./*.js": "./*.js",
12
13
  "./*": "./*.js"
13
14
  },
@@ -26,18 +27,18 @@
26
27
  },
27
28
  "dependencies": {
28
29
  "cors": "2.8.6",
29
- "express": "5.2.1",
30
+ "express": "5.3.0",
30
31
  "multer": "2.4.0",
31
32
  "path-to-regexp": "8.4.2",
32
33
  "tslib": "2.8.1"
33
34
  },
34
35
  "devDependencies": {
35
- "@nestjs/common": "12.1.2",
36
- "@nestjs/core": "12.1.2"
36
+ "@nestjs/common": "12.2.0",
37
+ "@nestjs/core": "12.2.0"
37
38
  },
38
39
  "peerDependencies": {
39
40
  "@nestjs/common": "^12.0.0",
40
41
  "@nestjs/core": "^12.0.0"
41
42
  },
42
- "gitHead": "8ab60aa1b2927fbe0e23135ffc65d610c623236a"
43
+ "gitHead": "af37111368de7fd2dfab0bdec2f530c7845510c2"
43
44
  }