@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.
- package/adapters/express-adapter.d.ts +18 -3
- package/adapters/express-adapter.js +107 -10
- package/adapters/utils/get-media-type-version.util.d.ts +4 -0
- package/adapters/utils/get-media-type-version.util.js +37 -3
- package/multer/interceptors/any-files.interceptor.js +5 -3
- package/multer/interceptors/file-fields.interceptor.js +5 -3
- package/multer/interceptors/file.interceptor.js +5 -3
- package/multer/interceptors/files.interceptor.js +5 -3
- package/multer/interceptors/no-files.interceptor.js +2 -2
- package/multer/interfaces/multer-options.interface.d.ts +15 -2
- package/multer/multer/multer.constants.d.ts +11 -0
- package/multer/multer/multer.constants.js +11 -0
- package/multer/multer/multer.utils.d.ts +15 -0
- package/multer/multer/multer.utils.js +62 -1
- package/package.json +6 -5
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
45
|
+
.flatMap(value => splitOutsideQuotes(value, ','));
|
|
16
46
|
for (const mediaRange of mediaRanges) {
|
|
17
|
-
const [, ...parameters] = mediaRange
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
31
|
-
*
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
36
|
-
"@nestjs/core": "12.
|
|
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": "
|
|
43
|
+
"gitHead": "af37111368de7fd2dfab0bdec2f530c7845510c2"
|
|
43
44
|
}
|