@nestjs/platform-fastify 12.1.3 → 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/fastify-adapter.d.ts +36 -9
- package/adapters/fastify-adapter.js +252 -35
- package/adapters/utils/get-media-type-version.util.d.ts +4 -0
- package/adapters/utils/get-media-type-version.util.js +37 -3
- package/multipart/interfaces/multipart-options.interface.d.ts +11 -9
- package/multipart/multipart/multipart-interceptor.factory.js +20 -4
- package/multipart/multipart/multipart.constants.d.ts +13 -0
- package/multipart/multipart/multipart.constants.js +13 -0
- package/multipart/multipart/multipart.parser.d.ts +6 -4
- package/multipart/multipart/multipart.parser.js +30 -15
- package/multipart/multipart/multipart.utils.js +5 -0
- package/multipart/storage/disk.storage.js +27 -2
- package/multipart/storage/memory.storage.js +12 -4
- package/package.json +6 -5
|
@@ -67,12 +67,15 @@ export declare class FastifyAdapter<TServer extends RawServerBase = RawServerDef
|
|
|
67
67
|
private readonly registeredContentTypes;
|
|
68
68
|
private onRequestHook?;
|
|
69
69
|
private onResponseHook?;
|
|
70
|
+
private securityHook?;
|
|
70
71
|
private isMiddieRegistered;
|
|
71
72
|
private multipartMode;
|
|
72
73
|
private multipartOptions;
|
|
73
74
|
private isMultipartRequested;
|
|
74
75
|
private pendingMiddlewares;
|
|
76
|
+
private readonly middieContexts;
|
|
75
77
|
private versioningOptions?;
|
|
78
|
+
private readonly versionExtractorErrors;
|
|
76
79
|
private readonly versionConstraint;
|
|
77
80
|
get isParserRegistered(): boolean;
|
|
78
81
|
constructor(instanceOrOptions?: TInstance | FastifyHttp2Options<any> | FastifyHttp2SecureOptions<any> | FastifyHttpsOptions<any> | FastifyHttpOptions<any> | FastifyAdapterBaseOptions<TServer>);
|
|
@@ -132,9 +135,9 @@ export declare class FastifyAdapter<TServer extends RawServerBase = RawServerDef
|
|
|
132
135
|
useStaticAssets(options: FastifyStaticOptions): any;
|
|
133
136
|
setViewEngine(options: FastifyViewOptions | string): any;
|
|
134
137
|
isHeadersSent(response: TRawResponse | TReply): boolean;
|
|
135
|
-
getHeader(response:
|
|
136
|
-
setHeader(response: TReply, name: string, value: string): FastifyReply<RouteGenericInterface, TServer, TRawRequest, TRawResponse, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
|
|
137
|
-
appendHeader(response:
|
|
138
|
+
getHeader(response: TRawResponse | TReply, name: string): string | number | string[] | undefined;
|
|
139
|
+
setHeader(response: TRawResponse | TReply, name: string, value: string): void | http.ServerResponse<http.IncomingMessage> | FastifyReply<RouteGenericInterface, TServer, TRawRequest, TRawResponse, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
|
|
140
|
+
appendHeader(response: TRawResponse | TReply, name: string, value: string): void | http.ServerResponse<http.IncomingMessage> | FastifyReply<RouteGenericInterface, TServer, TRawRequest, TRawResponse, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
|
|
138
141
|
getRequestHostname(request: TRequest): string;
|
|
139
142
|
getRequestMethod(request: TRequest): string;
|
|
140
143
|
getRequestUrl(request: TRequest): string;
|
|
@@ -142,21 +145,27 @@ export declare class FastifyAdapter<TServer extends RawServerBase = RawServerDef
|
|
|
142
145
|
enableCors(options?: FastifyCorsOptions): void;
|
|
143
146
|
/**
|
|
144
147
|
* Runs the request hook of the built-in HTTP security features in an
|
|
145
|
-
* `onRequest` hook: before middie (Nest middleware
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
148
|
+
* `onRequest` hook added by the constructor: before middie (Nest middleware
|
|
149
|
+
* and `app.use()` middleware, whenever they are registered), content-type
|
|
150
|
+
* parsing, guards and handlers, and also for unmatched routes. Headers set
|
|
151
|
+
* by the hook go to the Node.js response (`reply.raw`): Fastify merges them
|
|
152
|
+
* into every response it sends, errors and `404`s included, while
|
|
149
153
|
* `reply.header()` / `@Header()` values take precedence, and responses
|
|
150
|
-
* written to `reply.raw` directly (e.g. `@Sse()
|
|
151
|
-
* rejection goes to `done(error)`, i.e. to the Nest exception layer
|
|
154
|
+
* written to `reply.raw` directly (e.g. `@Sse()`, or middleware) carry them
|
|
155
|
+
* too. A rejection goes to `done(error)`, i.e. to the Nest exception layer
|
|
152
156
|
* installed with `setErrorHandler()`.
|
|
153
157
|
*/
|
|
154
158
|
registerSecurityHook(hook: SecurityRequestHook<TRequest>): void;
|
|
155
159
|
registerParserMiddleware(prefix?: string, rawBody?: boolean): void;
|
|
156
160
|
useBodyParser(type: string | string[] | RegExp, rawBody: boolean, options?: NestFastifyBodyParserOptions, parser?: FastifyBodyParser<Buffer, TServer>): void;
|
|
157
161
|
createMiddlewareFactory(requestMethod: RequestMethod): Promise<(path: string, callback: Function) => any>;
|
|
162
|
+
private runMiddleware;
|
|
158
163
|
getType(): string;
|
|
159
164
|
isRouteOrderSensitive(): boolean;
|
|
165
|
+
getRouteMatchingOptions(): {
|
|
166
|
+
caseSensitive: boolean;
|
|
167
|
+
ignoreTrailingSlash: boolean;
|
|
168
|
+
};
|
|
160
169
|
use(...args: any[]): any;
|
|
161
170
|
mapException(error: unknown): unknown;
|
|
162
171
|
private isHttpFastifyError;
|
|
@@ -205,6 +214,24 @@ export declare class FastifyAdapter<TServer extends RawServerBase = RawServerDef
|
|
|
205
214
|
* and leaves any other request target untouched.
|
|
206
215
|
*/
|
|
207
216
|
private getPathFromRequestTarget;
|
|
217
|
+
/**
|
|
218
|
+
* Whether the request target is in origin-form ("/users") or in the
|
|
219
|
+
* absolute-form of an "http(s)" URL ("http://host/users"), the only forms
|
|
220
|
+
* middleware paths can match.
|
|
221
|
+
*
|
|
222
|
+
* Node.js accepts other targets too (e.g. "GET *users HTTP/1.1").
|
|
223
|
+
* "find-my-way" routes those as if their first character were the leading
|
|
224
|
+
* "/" ("*users" is served by the "/users" route), while no middleware path
|
|
225
|
+
* matches them, so the route would run without its middleware. They are
|
|
226
|
+
* answered by the not-found handler instead, as with the ExpressAdapter,
|
|
227
|
+
* whose router never matches a path that does not start with "/".
|
|
228
|
+
*
|
|
229
|
+
* That includes the asterisk-form "OPTIONS *" (RFC 9112, section 3.2.4),
|
|
230
|
+
* which asks about the server as a whole rather than about a resource:
|
|
231
|
+
* Nest has no handler for it, and "find-my-way" would serve it with the
|
|
232
|
+
* "OPTIONS /" route.
|
|
233
|
+
*/
|
|
234
|
+
private isRoutableRequestTarget;
|
|
208
235
|
private trackOpenConnections;
|
|
209
236
|
private closeOpenConnections;
|
|
210
237
|
}
|
|
@@ -2,10 +2,12 @@ import { HttpException, HttpStatus, Logger, StreamableFile, VERSION_NEUTRAL, Ver
|
|
|
2
2
|
import { fastify, } from 'fastify';
|
|
3
3
|
import Reply from 'fastify/lib/reply.js';
|
|
4
4
|
import fastifySymbols from 'fastify/lib/symbols.js';
|
|
5
|
+
import * as http from 'http';
|
|
5
6
|
import * as net from 'net';
|
|
6
7
|
import { pathToRegexp } from 'path-to-regexp';
|
|
8
|
+
import { Readable } from 'stream';
|
|
7
9
|
import middie from '@fastify/middie';
|
|
8
|
-
import { loadPackage, tryLoadPackage, isNil, isString, isUndefined, } from '@nestjs/common/internal';
|
|
10
|
+
import { loadPackage, tryLoadPackage, isFunction, isNil, isString, isUndefined, } from '@nestjs/common/internal';
|
|
9
11
|
import { AbstractHttpAdapter } from '@nestjs/core';
|
|
10
12
|
import { LegacyRouteConverter } from '@nestjs/core/internal';
|
|
11
13
|
import { getMediaTypeVersion } from './utils/get-media-type-version.util.js';
|
|
@@ -45,12 +47,18 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
45
47
|
registeredContentTypes = new Set();
|
|
46
48
|
onRequestHook;
|
|
47
49
|
onResponseHook;
|
|
50
|
+
securityHook;
|
|
48
51
|
isMiddieRegistered;
|
|
49
52
|
multipartMode = 'auto';
|
|
50
53
|
multipartOptions = {};
|
|
51
54
|
isMultipartRequested = false;
|
|
52
55
|
pendingMiddlewares = [];
|
|
56
|
+
// The encapsulated contexts created since middie was registered (see
|
|
57
|
+
// `registerMiddie()`)
|
|
58
|
+
middieContexts = [];
|
|
53
59
|
versioningOptions;
|
|
60
|
+
// Exceptions thrown by the extractor of custom versioning, by raw request
|
|
61
|
+
versionExtractorErrors = new WeakMap();
|
|
54
62
|
versionConstraint = {
|
|
55
63
|
name: 'version',
|
|
56
64
|
validate(value) {
|
|
@@ -108,7 +116,16 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
108
116
|
}
|
|
109
117
|
// Custom Versioning Handler
|
|
110
118
|
else if (this.versioningOptions?.type === VersioningType.CUSTOM) {
|
|
111
|
-
|
|
119
|
+
try {
|
|
120
|
+
return this.versioningOptions.extractor(req);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
// Fastify routes the request, and so calls the extractor, before
|
|
124
|
+
// anything could catch the exception. It is passed to the error
|
|
125
|
+
// handler from an `onRequest` hook instead (see the constructor).
|
|
126
|
+
this.versionExtractorErrors.set(req, err);
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
112
129
|
}
|
|
113
130
|
return undefined;
|
|
114
131
|
},
|
|
@@ -122,6 +139,20 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
122
139
|
const instance = instanceOrOptions && instanceOrOptions.server
|
|
123
140
|
? instanceOrOptions
|
|
124
141
|
: fastify({
|
|
142
|
+
// The router answers a malformed URL (400) or an over-long route
|
|
143
|
+
// parameter (414) before any hook runs, so the security hook (see
|
|
144
|
+
// `registerSecurityHook()`) runs here, for its headers.
|
|
145
|
+
frameworkErrors: (error, request, reply) => {
|
|
146
|
+
try {
|
|
147
|
+
this.securityHook?.(request, reply.raw);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// The request is rejected either way, and a throw here would
|
|
151
|
+
// escape the router. `request.params` and `request.query`
|
|
152
|
+
// are null, so a CSRF `exclude` predicate may throw.
|
|
153
|
+
}
|
|
154
|
+
reply.send(error);
|
|
155
|
+
},
|
|
125
156
|
...instanceOrOptions,
|
|
126
157
|
routerOptions: {
|
|
127
158
|
...this.getTopLevelRouterOptions(instanceOrOptions),
|
|
@@ -176,6 +207,46 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
176
207
|
done();
|
|
177
208
|
}
|
|
178
209
|
});
|
|
210
|
+
// The request hook of `app.enableCsrfProtection()` and
|
|
211
|
+
// `app.useSecurityHeaders()` (see `registerSecurityHook()`). Added here
|
|
212
|
+
// for the same reason as the hook above: it must run before Nest
|
|
213
|
+
// middleware, and `NestFactory.create()` registers middie before either
|
|
214
|
+
// method can be called. After the `503` hook, as on Express.
|
|
215
|
+
this.instance.addHook('onRequest', (request, reply, done) => {
|
|
216
|
+
if (this.securityHook) {
|
|
217
|
+
done(this.securityHook(request, reply.raw));
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
done();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
// Passes requests whose target middleware cannot be matched against to
|
|
224
|
+
// the not-found handler (see `isRoutableRequestTarget()`). Added here,
|
|
225
|
+
// like the hooks above, so that it runs before Nest middleware, and
|
|
226
|
+
// after the security hook, so that the `404` carries its headers, as on
|
|
227
|
+
// Express. A request the router did not match is already on its way to
|
|
228
|
+
// the not-found handler.
|
|
229
|
+
this.instance.addHook('onRequest', (request, reply, done) => {
|
|
230
|
+
if (request.is404 || this.isRoutableRequestTarget(request.raw.url)) {
|
|
231
|
+
done();
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
reply.callNotFound();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
// Passes an exception thrown by the extractor of custom versioning while
|
|
238
|
+
// Fastify routed the request (see `versionConstraint`) to the error
|
|
239
|
+
// handler, for matched and unmatched routes alike. Added here, like the
|
|
240
|
+
// hooks above, so that it runs before Nest middleware.
|
|
241
|
+
this.instance.addHook('onRequest', (request, _reply, done) => {
|
|
242
|
+
if (!this.versionExtractorErrors.has(request.raw)) {
|
|
243
|
+
done();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const err = this.versionExtractorErrors.get(request.raw);
|
|
247
|
+
this.versionExtractorErrors.delete(request.raw);
|
|
248
|
+
done(err || new fastify.errorCodes.FST_ERR_SEND_UNDEFINED_ERR());
|
|
249
|
+
});
|
|
179
250
|
this.instance.addHook('onResponse', (request, reply, done) => {
|
|
180
251
|
if (this.onResponseHook) {
|
|
181
252
|
this.runLifecycleHook(this.onResponseHook, request, reply, done);
|
|
@@ -317,6 +388,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
317
388
|
return this.injectRouteOptions('UNLOCK', ...args);
|
|
318
389
|
}
|
|
319
390
|
all(...args) {
|
|
391
|
+
// Routes registered by Nest ("path, handler") get the route options
|
|
392
|
+
// (e.g. the version), with the same methods as "fastify.all()".
|
|
393
|
+
if (args.length === 2 && isFunction(args[1])) {
|
|
394
|
+
return this.injectRouteOptions(this.instance.supportedMethods, ...args);
|
|
395
|
+
}
|
|
320
396
|
const handlerRef = args[args.length - 1];
|
|
321
397
|
args[args.length - 1] = this.requireNonEmptyParams(args[0], handlerRef);
|
|
322
398
|
return this.instance.all(...args);
|
|
@@ -345,6 +421,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
345
421
|
fastifyReply.status(statusCode);
|
|
346
422
|
}
|
|
347
423
|
if (body instanceof StreamableFile) {
|
|
424
|
+
const stream = body.getStream();
|
|
425
|
+
// Once the reply was sent (e.g. by `handlerTimeout`), send() drops the
|
|
426
|
+
// stream without consuming or destroying it.
|
|
427
|
+
if (fastifyReply.sent && isFunction(stream?.destroy)) {
|
|
428
|
+
stream.destroy();
|
|
429
|
+
}
|
|
348
430
|
const streamHeaders = body.getHeaders();
|
|
349
431
|
if (fastifyReply.getHeader('Content-Type') === undefined &&
|
|
350
432
|
streamHeaders.type !== undefined) {
|
|
@@ -358,7 +440,29 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
358
440
|
streamHeaders.length !== undefined) {
|
|
359
441
|
fastifyReply.header('Content-Length', streamHeaders.length);
|
|
360
442
|
}
|
|
361
|
-
body =
|
|
443
|
+
body = stream;
|
|
444
|
+
// Fastify's HEAD route handler resumes a stream payload, and a route
|
|
445
|
+
// declared for HEAD (or a raw response) pipes it into a response that
|
|
446
|
+
// discards it, so the whole source would be read for nothing. Fastify
|
|
447
|
+
// gets an empty stream instead, as an empty body would reset
|
|
448
|
+
// Content-Length to 0.
|
|
449
|
+
if (fastifyReply.raw?.req?.method === 'HEAD' &&
|
|
450
|
+
isFunction(stream?.destroy)) {
|
|
451
|
+
stream.destroy();
|
|
452
|
+
body = Readable.from([]);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Fastify passes an Error payload to the error handler instead of
|
|
456
|
+
// sending it, and the reply around a raw response (e.g. the one
|
|
457
|
+
// middleware gets) has none. An Error body sent with an error status,
|
|
458
|
+
// as for `new UnauthorizedException(err)`, gets the standard body of the
|
|
459
|
+
// status instead, without the properties of the Error.
|
|
460
|
+
if (body instanceof Error &&
|
|
461
|
+
fastifyReply.statusCode >= HttpStatus.BAD_REQUEST) {
|
|
462
|
+
body = {
|
|
463
|
+
statusCode: fastifyReply.statusCode,
|
|
464
|
+
message: http.STATUS_CODES[fastifyReply.statusCode] ?? 'Error',
|
|
465
|
+
};
|
|
362
466
|
}
|
|
363
467
|
const responseContentType = fastifyReply.getHeader('Content-Type');
|
|
364
468
|
if (typeof responseContentType === 'string' &&
|
|
@@ -511,20 +615,40 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
511
615
|
return response.getHeader(name);
|
|
512
616
|
}
|
|
513
617
|
setHeader(response, name, value) {
|
|
514
|
-
|
|
618
|
+
if (this.isNativeResponse(response)) {
|
|
619
|
+
return response.setHeader(name, value);
|
|
620
|
+
}
|
|
621
|
+
const reply = response;
|
|
622
|
+
return reply.header(name, value);
|
|
515
623
|
}
|
|
516
624
|
appendHeader(response, name, value) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return response.
|
|
625
|
+
if (this.isNativeResponse(response)) {
|
|
626
|
+
const current = response.getHeader(name);
|
|
627
|
+
return response.setHeader(name, current === undefined ? value : [current, value].flat().map(String));
|
|
520
628
|
}
|
|
521
|
-
|
|
522
|
-
//
|
|
629
|
+
const reply = response;
|
|
630
|
+
// A set-cookie value can live on the reply or only on the raw response
|
|
631
|
+
// (e.g., set by a middleware). Reply.getHeader() reads both, and the
|
|
632
|
+
// merged list is written to both: reply.send() sends the reply headers,
|
|
633
|
+
// while a hijacked reply or a handler that ends reply.raw sends the raw
|
|
634
|
+
// ones.
|
|
523
635
|
if (String(name).toLowerCase() === 'set-cookie') {
|
|
524
|
-
|
|
636
|
+
const current = reply.getHeader(name);
|
|
637
|
+
const values = current === undefined ? [value] : [current, value].flat().map(String);
|
|
638
|
+
reply.removeHeader(name);
|
|
639
|
+
reply.header(name, values);
|
|
640
|
+
if (!reply.raw.headersSent) {
|
|
641
|
+
// A copy, so a later reply.header() call does not change the raw header
|
|
642
|
+
reply.raw.setHeader(name, [...values]);
|
|
643
|
+
}
|
|
644
|
+
return reply;
|
|
645
|
+
}
|
|
646
|
+
const current = reply.getHeader(name);
|
|
647
|
+
if (current === undefined) {
|
|
648
|
+
return reply.header(name, value);
|
|
525
649
|
}
|
|
526
650
|
const values = Array.isArray(current) ? current : [current];
|
|
527
|
-
return
|
|
651
|
+
return reply.header(name, values.concat(value));
|
|
528
652
|
}
|
|
529
653
|
getRequestHostname(request) {
|
|
530
654
|
return request.hostname;
|
|
@@ -540,19 +664,18 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
540
664
|
}
|
|
541
665
|
/**
|
|
542
666
|
* Runs the request hook of the built-in HTTP security features in an
|
|
543
|
-
* `onRequest` hook: before middie (Nest middleware
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
667
|
+
* `onRequest` hook added by the constructor: before middie (Nest middleware
|
|
668
|
+
* and `app.use()` middleware, whenever they are registered), content-type
|
|
669
|
+
* parsing, guards and handlers, and also for unmatched routes. Headers set
|
|
670
|
+
* by the hook go to the Node.js response (`reply.raw`): Fastify merges them
|
|
671
|
+
* into every response it sends, errors and `404`s included, while
|
|
547
672
|
* `reply.header()` / `@Header()` values take precedence, and responses
|
|
548
|
-
* written to `reply.raw` directly (e.g. `@Sse()
|
|
549
|
-
* rejection goes to `done(error)`, i.e. to the Nest exception layer
|
|
673
|
+
* written to `reply.raw` directly (e.g. `@Sse()`, or middleware) carry them
|
|
674
|
+
* too. A rejection goes to `done(error)`, i.e. to the Nest exception layer
|
|
550
675
|
* installed with `setErrorHandler()`.
|
|
551
676
|
*/
|
|
552
677
|
registerSecurityHook(hook) {
|
|
553
|
-
this.
|
|
554
|
-
done(hook(request, reply.raw));
|
|
555
|
-
});
|
|
678
|
+
this.securityHook = hook;
|
|
556
679
|
}
|
|
557
680
|
registerParserMiddleware(prefix, rawBody) {
|
|
558
681
|
if (this._isParserRegistered) {
|
|
@@ -616,9 +739,13 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
616
739
|
re = hasEndOfStringCharacter
|
|
617
740
|
? new RegExp(re.source + '$', re.flags)
|
|
618
741
|
: re;
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
|
|
742
|
+
// A case-insensitive router lower-cases the decoded path, where some
|
|
743
|
+
// characters only fold to ASCII ("%E2%84%AA", KELVIN SIGN, is routed
|
|
744
|
+
// as "k"). The RegExp of @fastify/middie does not fold them, so on
|
|
745
|
+
// such a router the middleware is registered for every path and the
|
|
746
|
+
// path is only matched below.
|
|
747
|
+
const { caseSensitive } = this.getRouteMatchingOptions();
|
|
748
|
+
const createMiddleware = (stripsPath) => (req, res, next) => {
|
|
622
749
|
const queryParamsIndex = req.originalUrl.indexOf('?');
|
|
623
750
|
let pathname = queryParamsIndex >= 0
|
|
624
751
|
? req.originalUrl.slice(0, queryParamsIndex)
|
|
@@ -631,9 +758,28 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
631
758
|
if (!re.exec(pathToCheck)) {
|
|
632
759
|
return next();
|
|
633
760
|
}
|
|
761
|
+
if (stripsPath) {
|
|
762
|
+
// Strips the path from "req.url", as @fastify/middie does
|
|
763
|
+
// when it matches the path itself
|
|
764
|
+
const suffixIndex = req.originalUrl.search(/[?#]/);
|
|
765
|
+
req.url =
|
|
766
|
+
suffixIndex >= 0
|
|
767
|
+
? `/${req.originalUrl.slice(suffixIndex)}`
|
|
768
|
+
: '/';
|
|
769
|
+
}
|
|
634
770
|
}
|
|
635
|
-
|
|
636
|
-
}
|
|
771
|
+
this.runMiddleware(callback, req, res, next);
|
|
772
|
+
};
|
|
773
|
+
// The following type assertion is valid as we use import('@fastify/middie') rather than require('@fastify/middie')
|
|
774
|
+
// ref https://github.com/fastify/middie/pull/55
|
|
775
|
+
this.instance.use(caseSensitive ? normalizedPath : '/', createMiddleware(!caseSensitive));
|
|
776
|
+
// The contexts created since middie was registered have a copy of the
|
|
777
|
+
// middleware of their parent without this one (see `registerMiddie()`).
|
|
778
|
+
// It is registered there without its path, which middie would prefix
|
|
779
|
+
// with the one of the context.
|
|
780
|
+
for (const context of this.middieContexts) {
|
|
781
|
+
context.use(createMiddleware(true));
|
|
782
|
+
}
|
|
637
783
|
}
|
|
638
784
|
catch (e) {
|
|
639
785
|
if (e instanceof TypeError) {
|
|
@@ -643,12 +789,53 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
643
789
|
}
|
|
644
790
|
};
|
|
645
791
|
}
|
|
792
|
+
runMiddleware(middleware, req, res, next) {
|
|
793
|
+
let completed = false;
|
|
794
|
+
const nextOnce = (err) => {
|
|
795
|
+
if (completed)
|
|
796
|
+
return;
|
|
797
|
+
completed = true;
|
|
798
|
+
next(err);
|
|
799
|
+
};
|
|
800
|
+
try {
|
|
801
|
+
// middie discards the return value, so the promise is observed here:
|
|
802
|
+
// the proxies of `RouterProxy` reject when the exception layer throws.
|
|
803
|
+
// Its resolution is not passed to `next`, as a middleware that answers
|
|
804
|
+
// the request does not call it.
|
|
805
|
+
const result = middleware(req, res, nextOnce);
|
|
806
|
+
if (result && typeof result.then === 'function') {
|
|
807
|
+
result.then(undefined, (err) => {
|
|
808
|
+
if (completed) {
|
|
809
|
+
this.logger.error(err);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
nextOnce(err || new fastify.errorCodes.FST_ERR_SEND_UNDEFINED_ERR());
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
catch (err) {
|
|
817
|
+
nextOnce(err);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
646
820
|
getType() {
|
|
647
821
|
return 'fastify';
|
|
648
822
|
}
|
|
649
823
|
isRouteOrderSensitive() {
|
|
650
824
|
return false;
|
|
651
825
|
}
|
|
826
|
+
// The return type is spelled out (rather than "RouteMatchingOptions") so
|
|
827
|
+
// that the declarations still compile against an older "@nestjs/common"
|
|
828
|
+
getRouteMatchingOptions() {
|
|
829
|
+
const initialConfig = this.instance.initialConfig;
|
|
830
|
+
// Absent when a Fastify instance created without it is passed in
|
|
831
|
+
const routerOptions = (initialConfig.routerOptions ??
|
|
832
|
+
{});
|
|
833
|
+
return {
|
|
834
|
+
caseSensitive: !(routerOptions.caseSensitive === false ||
|
|
835
|
+
initialConfig.caseSensitive === false),
|
|
836
|
+
ignoreTrailingSlash: !!(routerOptions.ignoreTrailingSlash || initialConfig.ignoreTrailingSlash),
|
|
837
|
+
};
|
|
838
|
+
}
|
|
652
839
|
use(...args) {
|
|
653
840
|
// Fastify requires @fastify/middie plugin to be registered before middleware can be used.
|
|
654
841
|
// If middie is not registered yet, we queue the middleware and register it later during init.
|
|
@@ -732,6 +919,15 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
732
919
|
async registerMiddie() {
|
|
733
920
|
this.isMiddieRegistered = true;
|
|
734
921
|
await this.register(middie);
|
|
922
|
+
// middie gives each encapsulated context created from now on a copy of
|
|
923
|
+
// the middleware of its parent, as it is at that time. As
|
|
924
|
+
// `NestFactory.create()` registers middie before `main.ts` runs, a
|
|
925
|
+
// plugin registered there with an awaited `register()` gets a context
|
|
926
|
+
// without the Nest middleware `app.init()` registers later, so
|
|
927
|
+
// `createMiddlewareFactory()` adds it to the contexts tracked here.
|
|
928
|
+
this.instance.addHook('onRegister', instance => {
|
|
929
|
+
this.middieContexts.push(instance);
|
|
930
|
+
});
|
|
735
931
|
}
|
|
736
932
|
getRequestOriginalUrl(rawRequest) {
|
|
737
933
|
return rawRequest.originalUrl || rawRequest.url;
|
|
@@ -751,7 +947,8 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
751
947
|
url: args[0],
|
|
752
948
|
handler: this.requireNonEmptyParams(args[0], handlerRef),
|
|
753
949
|
};
|
|
754
|
-
if (
|
|
950
|
+
if (isString(routerMethodKey) &&
|
|
951
|
+
!this.instance.supportedMethods.includes(routerMethodKey)) {
|
|
755
952
|
this.instance.addHttpMethod(routerMethodKey, { hasBody: true });
|
|
756
953
|
}
|
|
757
954
|
if (isVersioned || hasConstraints || hasConfig || hasSchema) {
|
|
@@ -837,7 +1034,8 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
837
1034
|
}
|
|
838
1035
|
sanitizeUrl(url) {
|
|
839
1036
|
const initialConfig = this.instance.initialConfig;
|
|
840
|
-
const routerOptions = initialConfig.routerOptions
|
|
1037
|
+
const routerOptions = (initialConfig.routerOptions ??
|
|
1038
|
+
{});
|
|
841
1039
|
// Absolute-form request targets ("GET http://host/path HTTP/1.1") must be
|
|
842
1040
|
// resolved to their path before any other normalization, as the Fastify
|
|
843
1041
|
// router does, so that middleware and routes always match the same path.
|
|
@@ -846,16 +1044,15 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
846
1044
|
initialConfig.ignoreDuplicateSlashes) {
|
|
847
1045
|
url = this.removeDuplicateSlashes(url);
|
|
848
1046
|
}
|
|
849
|
-
|
|
850
|
-
|
|
1047
|
+
const { caseSensitive, ignoreTrailingSlash } = this.getRouteMatchingOptions();
|
|
1048
|
+
if (ignoreTrailingSlash) {
|
|
851
1049
|
url = this.trimLastSlash(url);
|
|
852
1050
|
}
|
|
853
|
-
|
|
854
|
-
initialConfig.
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
return
|
|
858
|
-
initialConfig.useSemicolonDelimiter).path;
|
|
1051
|
+
const { path } = safeDecodeURI(url, routerOptions.useSemicolonDelimiter ||
|
|
1052
|
+
initialConfig.useSemicolonDelimiter);
|
|
1053
|
+
// Lower-cased once decoded, as the router does: "%E2%84%AA" (KELVIN
|
|
1054
|
+
// SIGN) only lower-cases to "k" then
|
|
1055
|
+
return caseSensitive ? path : path.toLowerCase();
|
|
859
1056
|
}
|
|
860
1057
|
removeDuplicateSlashes(path) {
|
|
861
1058
|
const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
|
|
@@ -894,6 +1091,26 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
894
1091
|
}
|
|
895
1092
|
return pathStart === -1 ? '/' : url.slice(pathStart);
|
|
896
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Whether the request target is in origin-form ("/users") or in the
|
|
1096
|
+
* absolute-form of an "http(s)" URL ("http://host/users"), the only forms
|
|
1097
|
+
* middleware paths can match.
|
|
1098
|
+
*
|
|
1099
|
+
* Node.js accepts other targets too (e.g. "GET *users HTTP/1.1").
|
|
1100
|
+
* "find-my-way" routes those as if their first character were the leading
|
|
1101
|
+
* "/" ("*users" is served by the "/users" route), while no middleware path
|
|
1102
|
+
* matches them, so the route would run without its middleware. They are
|
|
1103
|
+
* answered by the not-found handler instead, as with the ExpressAdapter,
|
|
1104
|
+
* whose router never matches a path that does not start with "/".
|
|
1105
|
+
*
|
|
1106
|
+
* That includes the asterisk-form "OPTIONS *" (RFC 9112, section 3.2.4),
|
|
1107
|
+
* which asks about the server as a whole rather than about a resource:
|
|
1108
|
+
* Nest has no handler for it, and "find-my-way" would serve it with the
|
|
1109
|
+
* "OPTIONS /" route.
|
|
1110
|
+
*/
|
|
1111
|
+
isRoutableRequestTarget(url) {
|
|
1112
|
+
return !!url && this.getPathFromRequestTarget(url).charCodeAt(0) === 47;
|
|
1113
|
+
}
|
|
897
1114
|
trackOpenConnections() {
|
|
898
1115
|
const track = (socket) => {
|
|
899
1116
|
if (this.isClosing) {
|
|
@@ -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
|
}
|
|
@@ -12,11 +12,11 @@ export interface MultipartStorageEngine {
|
|
|
12
12
|
/**
|
|
13
13
|
* Same keys as platform-express's `MulterLimits`.
|
|
14
14
|
*
|
|
15
|
-
* Limits that are not set
|
|
16
|
-
* registered with, and then to its own
|
|
17
|
-
* defaults `fileSize` and `parts` to
|
|
18
|
-
* `
|
|
19
|
-
* and `parts` at 1000.
|
|
15
|
+
* Limits that are not set, or set to `null` or `undefined`, fall back to the
|
|
16
|
+
* options `@fastify/multipart` was registered with, and then to its own
|
|
17
|
+
* defaults. Unlike multer (which defaults `fileSize` and `parts` to
|
|
18
|
+
* `Infinity`), `@fastify/multipart` caps `fileSize` at the Fastify
|
|
19
|
+
* instance's `bodyLimit` (1 MiB unless configured) and `parts` at 1000.
|
|
20
20
|
*
|
|
21
21
|
* @publicApi
|
|
22
22
|
*/
|
|
@@ -40,15 +40,17 @@ export interface MultipartLimits {
|
|
|
40
40
|
/** For multipart forms, the max number of header key => value pairs to parse (Default: 2000) */
|
|
41
41
|
headerPairs?: number;
|
|
42
42
|
/**
|
|
43
|
-
* Max nesting depth of field names; `a[b][c]` has 2 levels (Default:
|
|
44
|
-
* Infinity).
|
|
43
|
+
* Max nesting depth of field names; `a[b][c]` has 2 levels (Default: 32,
|
|
44
|
+
* where multer's own default is Infinity). Set it to `Infinity` to lift
|
|
45
|
+
* the limit. Like `fieldNameSize`, it does not fall back to the options
|
|
45
46
|
* `@fastify/multipart` was registered with.
|
|
46
47
|
*/
|
|
47
48
|
fieldNestingDepth?: number;
|
|
48
49
|
/**
|
|
49
50
|
* Max numeric array index accepted in field names; `a[3]` uses index 3
|
|
50
|
-
* (Default:
|
|
51
|
-
*
|
|
51
|
+
* (Default: 1000, where multer's own default is Infinity). Set it to
|
|
52
|
+
* `Infinity` to lift the limit. Like `fieldNameSize`, it does not fall back
|
|
53
|
+
* to the options `@fastify/multipart` was registered with.
|
|
52
54
|
*/
|
|
53
55
|
fieldArrayIndexLimit?: number;
|
|
54
56
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { __decorate, __metadata, __param } from "tslib";
|
|
2
2
|
import { Inject, mixin, Optional, StreamableFile, } from '@nestjs/common';
|
|
3
3
|
import { HttpAdapterHost } from '@nestjs/core';
|
|
4
|
+
import { isRejectedByPipes } from '@nestjs/core/internal';
|
|
4
5
|
import { catchError, concatMap, from, throwError } from 'rxjs';
|
|
6
|
+
import { finished } from 'stream';
|
|
5
7
|
import { MULTIPART_MODULE_OPTIONS } from '../files.constants.js';
|
|
6
8
|
import { MISSING_PLUGIN_MESSAGE, PLUGIN_NOT_REGISTERED_AT_STARTUP_MESSAGE, WRONG_ADAPTER_MESSAGE, } from './multipart.constants.js';
|
|
7
|
-
import { drainRequest, processMultipart, processStream, resolveStorage, validateLimits, } from './multipart.parser.js';
|
|
9
|
+
import { drainRequest, processMultipart, processStream, removeFiles, resolveStorage, validateLimits, } from './multipart.parser.js';
|
|
8
10
|
import { mergeMultipartOptions } from './multipart.utils.js';
|
|
9
11
|
const verifiedInstances = new WeakSet();
|
|
10
12
|
/**
|
|
@@ -78,9 +80,23 @@ export function createMultipartInterceptor(spec, localOptions) {
|
|
|
78
80
|
this.storage = resolveStorage(this.options);
|
|
79
81
|
}
|
|
80
82
|
async intercept(context, next) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const ctx = context.switchToHttp();
|
|
84
|
+
const req = ctx.getRequest();
|
|
85
|
+
const stored = await processMultipart(req, spec, this.options, this.storage);
|
|
86
|
+
return next.handle().pipe(
|
|
87
|
+
// When the route's pipes reject the request (e.g. `ParseFilePipe`),
|
|
88
|
+
// the route handler never sees the files, and nothing else would
|
|
89
|
+
// remove them. They are removed once the response has been sent, as
|
|
90
|
+
// an exception filter may still use (or remove) them.
|
|
91
|
+
catchError(err => {
|
|
92
|
+
if (isRejectedByPipes(req)) {
|
|
93
|
+
const reply = ctx.getResponse();
|
|
94
|
+
finished(reply.raw, () => {
|
|
95
|
+
removeFiles(this.storage, req, stored).catch(() => undefined);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return throwError(() => err);
|
|
99
|
+
}));
|
|
84
100
|
}
|
|
85
101
|
};
|
|
86
102
|
MixinInterceptor = __decorate([
|
|
@@ -18,6 +18,19 @@ export declare const multerExceptions: {
|
|
|
18
18
|
readonly INVALID_FIELD_NAME: "Invalid field name";
|
|
19
19
|
};
|
|
20
20
|
export type MultipartErrorCode = keyof typeof multerExceptions;
|
|
21
|
+
/**
|
|
22
|
+
* Defaults of the field name limits that multer leaves at `Infinity`, which
|
|
23
|
+
* let a single small request build an arbitrarily deep `req.body`
|
|
24
|
+
* (`fieldNestingDepth`) or arrays of arbitrary length (`fieldArrayIndexLimit`).
|
|
25
|
+
* `32` is the nesting depth `body-parser` allows in the urlencoded bodies the
|
|
26
|
+
* ExpressAdapter parses; `1000` is the number of parameters it parses, and
|
|
27
|
+
* the number of parts `@fastify/multipart` accepts. Kept identical to
|
|
28
|
+
* platform-express's `DEFAULT_FIELD_NAME_LIMITS`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_FIELD_NAME_LIMITS: {
|
|
31
|
+
readonly fieldNestingDepth: 32;
|
|
32
|
+
readonly fieldArrayIndexLimit: 1000;
|
|
33
|
+
};
|
|
21
34
|
/**
|
|
22
35
|
* The messages platform-express reports for busboy 1.x parser errors
|
|
23
36
|
* (prefixed with "Multipart: "), which is what multer bundles.
|
|
@@ -20,6 +20,19 @@ export const multerExceptions = {
|
|
|
20
20
|
// field names that would pollute a prototype, which multer accepts.
|
|
21
21
|
INVALID_FIELD_NAME: 'Invalid field name',
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Defaults of the field name limits that multer leaves at `Infinity`, which
|
|
25
|
+
* let a single small request build an arbitrarily deep `req.body`
|
|
26
|
+
* (`fieldNestingDepth`) or arrays of arbitrary length (`fieldArrayIndexLimit`).
|
|
27
|
+
* `32` is the nesting depth `body-parser` allows in the urlencoded bodies the
|
|
28
|
+
* ExpressAdapter parses; `1000` is the number of parameters it parses, and
|
|
29
|
+
* the number of parts `@fastify/multipart` accepts. Kept identical to
|
|
30
|
+
* platform-express's `DEFAULT_FIELD_NAME_LIMITS`.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_FIELD_NAME_LIMITS = {
|
|
33
|
+
fieldNestingDepth: 32,
|
|
34
|
+
fieldArrayIndexLimit: 1000,
|
|
35
|
+
};
|
|
23
36
|
/**
|
|
24
37
|
* The messages platform-express reports for busboy 1.x parser errors
|
|
25
38
|
* (prefixed with "Multipart: "), which is what multer bundles.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FastifyRequest } from 'fastify';
|
|
2
2
|
import type { IncomingMessage } from 'http';
|
|
3
|
-
import type { MultipartField, MultipartLimits, MultipartOptions, MultipartStorageEngine } from '../interfaces/index.js';
|
|
3
|
+
import type { MultipartField, MultipartLimits, MultipartOptions, MultipartStorageEngine, UploadedMultipartFile } from '../interfaces/index.js';
|
|
4
4
|
/**
|
|
5
5
|
* Where accepted files end up, named after multer's `FileAppender` strategies.
|
|
6
6
|
*/
|
|
@@ -12,6 +12,7 @@ export interface UploadSpec {
|
|
|
12
12
|
}
|
|
13
13
|
/** Same resolution order as multer: `storage`, then `dest`, then memory. */
|
|
14
14
|
export declare function resolveStorage(options: MultipartOptions): MultipartStorageEngine;
|
|
15
|
+
export declare function removeFiles(storage: MultipartStorageEngine, req: FastifyRequest, files: UploadedMultipartFile[]): Promise<void[]>;
|
|
15
16
|
/**
|
|
16
17
|
* Stops parsing and discards the rest of the body, so that an error response
|
|
17
18
|
* does not race a client that is still uploading (multer does the same
|
|
@@ -26,10 +27,11 @@ export declare function drainRequest(raw: IncomingMessage): Promise<void>;
|
|
|
26
27
|
export declare function validateLimits(limits: MultipartLimits | undefined): void;
|
|
27
28
|
/**
|
|
28
29
|
* Parses the multipart body with multer's semantics and populates
|
|
29
|
-
* `req.body` and `req.file` / `req.files
|
|
30
|
-
* are passed through untouched, as multer
|
|
30
|
+
* `req.body` and `req.file` / `req.files`, and returns the stored files.
|
|
31
|
+
* Requests that are not multipart are passed through untouched, as multer
|
|
32
|
+
* does.
|
|
31
33
|
*/
|
|
32
|
-
export declare function processMultipart(request: FastifyRequest, spec: UploadSpec, options: MultipartOptions, storage: MultipartStorageEngine): Promise<
|
|
34
|
+
export declare function processMultipart(request: FastifyRequest, spec: UploadSpec, options: MultipartOptions, storage: MultipartStorageEngine): Promise<UploadedMultipartFile[]>;
|
|
33
35
|
/**
|
|
34
36
|
* Parses up to and including the first file in `fieldName`, then stops and
|
|
35
37
|
* hands that file's stream to the route handler (`req.file.stream`). Text
|
|
@@ -2,7 +2,7 @@ import { BadRequestException, PayloadTooLargeException } from '@nestjs/common';
|
|
|
2
2
|
import { diskStorage } from '../storage/disk.storage.js';
|
|
3
3
|
import { memoryStorage } from '../storage/memory.storage.js';
|
|
4
4
|
import { appendField } from './append-field.util.js';
|
|
5
|
-
import { busboyExceptions, MISSING_PLUGIN_MESSAGE, multerExceptions, } from './multipart.constants.js';
|
|
5
|
+
import { busboyExceptions, DEFAULT_FIELD_NAME_LIMITS, MISSING_PLUGIN_MESSAGE, multerExceptions, } from './multipart.constants.js';
|
|
6
6
|
import { MultipartError } from './multipart.error.js';
|
|
7
7
|
import { transformException } from './multipart.utils.js';
|
|
8
8
|
/** Same resolution order as multer: `storage`, then `dest`, then memory. */
|
|
@@ -22,15 +22,18 @@ function assertPluginRegistered(req) {
|
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Per-route options handed to `req.parts()`; `@fastify/multipart` deep-merges
|
|
25
|
-
* them over the options the plugin was registered with.
|
|
26
|
-
*
|
|
25
|
+
* them over the options the plugin was registered with. Limits set to `null`
|
|
26
|
+
* or `undefined` are left out, as the merge would copy them over the
|
|
27
|
+
* plugin's limits (and its `fileSize` and `parts` defaults), which
|
|
28
|
+
* `@fastify/busboy` then reads as no limit. `@fastify/busboy` treats
|
|
29
|
+
* `fileSize` and `parts` as "max allowed" (it fires once a limit is
|
|
27
30
|
* exceeded), so unlike multer there is no `+1` adjustment. Truncation is
|
|
28
31
|
* detected here (`file.truncated`), so the plugin's own throw is disabled.
|
|
29
32
|
*/
|
|
30
33
|
function toBusboyConfig(limits, options) {
|
|
31
34
|
const config = { throwFileSizeLimit: false };
|
|
32
35
|
if (limits) {
|
|
33
|
-
config.limits =
|
|
36
|
+
config.limits = Object.fromEntries(Object.entries(limits).filter(([, value]) => value != null));
|
|
34
37
|
}
|
|
35
38
|
if (options.preservePath !== undefined) {
|
|
36
39
|
config.preservePath = options.preservePath;
|
|
@@ -86,9 +89,12 @@ const ABORTED = Symbol('aborted');
|
|
|
86
89
|
* the engine (e.g. by a `pipeline()` to a file that cannot be written), which
|
|
87
90
|
* reports that error itself.
|
|
88
91
|
*
|
|
89
|
-
* Whatever the engine stores after an abort is removed
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
+
* Whatever the engine stores after an abort is removed, as multer does. The
|
|
93
|
+
* file at `path` (which engines set before writing, by multer's convention)
|
|
94
|
+
* is removed right away, as an engine waiting on `pipeline()` never calls
|
|
95
|
+
* back for a stream destroyed after its last chunk; removing it clears
|
|
96
|
+
* `path`. What the engine stored is removed once it calls back: what it
|
|
97
|
+
* returned when it succeeds, and the file at `path` when it fails.
|
|
92
98
|
*/
|
|
93
99
|
function storeFile(storage, req, file) {
|
|
94
100
|
return new Promise((resolve, reject) => {
|
|
@@ -101,6 +107,10 @@ function storeFile(storage, req, file) {
|
|
|
101
107
|
if (!stream.readableEnded && !stream.errored) {
|
|
102
108
|
aborted = true;
|
|
103
109
|
resolve(ABORTED);
|
|
110
|
+
const partial = file;
|
|
111
|
+
if (partial.path) {
|
|
112
|
+
removeFiles(storage, req, [partial]).catch(() => undefined);
|
|
113
|
+
}
|
|
104
114
|
}
|
|
105
115
|
};
|
|
106
116
|
stream.once('close', onClose);
|
|
@@ -116,7 +126,7 @@ function storeFile(storage, req, file) {
|
|
|
116
126
|
});
|
|
117
127
|
});
|
|
118
128
|
}
|
|
119
|
-
function removeFiles(storage, req, files) {
|
|
129
|
+
export function removeFiles(storage, req, files) {
|
|
120
130
|
return Promise.all(files.map(file => new Promise(resolve => storage._removeFile(req, file, () => resolve()))));
|
|
121
131
|
}
|
|
122
132
|
/**
|
|
@@ -216,14 +226,17 @@ function exceedsArrayIndexLimit(name, limit) {
|
|
|
216
226
|
/**
|
|
217
227
|
* `fieldNestingDepth` and `fieldArrayIndexLimit` are multer's own limits
|
|
218
228
|
* (busboy knows neither), so they are checked here, against the decoded name.
|
|
229
|
+
* Unlike multer, they default to `DEFAULT_FIELD_NAME_LIMITS`.
|
|
219
230
|
*/
|
|
220
231
|
function checkFieldNameShape(name, limits) {
|
|
221
|
-
|
|
222
|
-
|
|
232
|
+
const nestingDepth = limits?.fieldNestingDepth ?? DEFAULT_FIELD_NAME_LIMITS.fieldNestingDepth;
|
|
233
|
+
if (name.split('[').length - 1 > nestingDepth) {
|
|
223
234
|
throw new MultipartError('LIMIT_FIELD_NESTING', name);
|
|
224
235
|
}
|
|
225
|
-
|
|
226
|
-
|
|
236
|
+
const arrayIndexLimit = limits?.fieldArrayIndexLimit ??
|
|
237
|
+
DEFAULT_FIELD_NAME_LIMITS.fieldArrayIndexLimit;
|
|
238
|
+
if (arrayIndexLimit !== Infinity &&
|
|
239
|
+
exceedsArrayIndexLimit(name, arrayIndexLimit)) {
|
|
227
240
|
throw new MultipartError('LIMIT_FIELD_ARRAY_INDEX', name);
|
|
228
241
|
}
|
|
229
242
|
}
|
|
@@ -260,15 +273,16 @@ function readFilePart(part, limits) {
|
|
|
260
273
|
}
|
|
261
274
|
/**
|
|
262
275
|
* Parses the multipart body with multer's semantics and populates
|
|
263
|
-
* `req.body` and `req.file` / `req.files
|
|
264
|
-
* are passed through untouched, as multer
|
|
276
|
+
* `req.body` and `req.file` / `req.files`, and returns the stored files.
|
|
277
|
+
* Requests that are not multipart are passed through untouched, as multer
|
|
278
|
+
* does.
|
|
265
279
|
*/
|
|
266
280
|
export async function processMultipart(request, spec, options, storage) {
|
|
267
281
|
const req = request;
|
|
268
282
|
assertPluginRegistered(req);
|
|
269
283
|
resetFiles(req);
|
|
270
284
|
if (!req.isMultipart()) {
|
|
271
|
-
return;
|
|
285
|
+
return [];
|
|
272
286
|
}
|
|
273
287
|
const filesLeft = new Map();
|
|
274
288
|
if (spec.fields !== 'ANY') {
|
|
@@ -339,6 +353,7 @@ export async function processMultipart(request, spec, options, storage) {
|
|
|
339
353
|
break;
|
|
340
354
|
}
|
|
341
355
|
}
|
|
356
|
+
return stored;
|
|
342
357
|
}
|
|
343
358
|
/**
|
|
344
359
|
* Parses up to and including the first file in `fieldName`, then stops and
|
|
@@ -13,6 +13,11 @@ export function mergeMultipartOptions(options = {}, localOptions = {}) {
|
|
|
13
13
|
if (isPlainObject(options.limits) && isPlainObject(localOptions.limits)) {
|
|
14
14
|
merged.limits = { ...options.limits, ...localOptions.limits };
|
|
15
15
|
}
|
|
16
|
+
// `dest` and `storage` both say where files go, and `resolveStorage()`
|
|
17
|
+
// prefers `storage`, so a global storage engine would shadow a local `dest`.
|
|
18
|
+
if (localOptions.dest && !localOptions.storage) {
|
|
19
|
+
delete merged.storage;
|
|
20
|
+
}
|
|
16
21
|
return merged;
|
|
17
22
|
}
|
|
18
23
|
/**
|
|
@@ -3,10 +3,24 @@ import { createWriteStream, mkdirSync } from 'fs';
|
|
|
3
3
|
import { unlink } from 'fs/promises';
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
-
import { pipeline } from 'stream/promises';
|
|
6
|
+
import { finished, pipeline } from 'stream/promises';
|
|
7
7
|
function resolveName(resolver, req, file) {
|
|
8
8
|
return new Promise((resolve, reject) => resolver(req, file, (err, value) => (err ? reject(err) : resolve(value))));
|
|
9
9
|
}
|
|
10
|
+
const STREAM_DESTROYED_MESSAGE = 'File stream was destroyed';
|
|
11
|
+
/**
|
|
12
|
+
* Rejects once `stream` is destroyed before its end without an error, as
|
|
13
|
+
* `@fastify/multipart` does when the form is cut off. `pipeline()` never
|
|
14
|
+
* settles then if the last chunk had already been pushed: it waits for an
|
|
15
|
+
* 'end' that is never emitted, with the file still open.
|
|
16
|
+
*/
|
|
17
|
+
function rejectWhenCutOff(stream) {
|
|
18
|
+
return new Promise((_resolve, reject) => stream.once('close', () => {
|
|
19
|
+
if (!stream.readableEnded && !stream.errored) {
|
|
20
|
+
reject(new Error(STREAM_DESTROYED_MESSAGE));
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
10
24
|
/**
|
|
11
25
|
* Writes each file to disk, with multer's `diskStorage` defaults and file
|
|
12
26
|
* shape (`destination`, `filename`, `path`, `size`).
|
|
@@ -30,14 +44,25 @@ export function diskStorage(options = {}) {
|
|
|
30
44
|
const store = async (req, file) => {
|
|
31
45
|
const destination = await resolveName(resolveDestination, req, file);
|
|
32
46
|
const filename = await resolveName(resolveFilename, req, file);
|
|
47
|
+
// As in multer, no file is created for a stream destroyed in the meantime.
|
|
48
|
+
if (file.stream.destroyed) {
|
|
49
|
+
throw new Error(STREAM_DESTROYED_MESSAGE);
|
|
50
|
+
}
|
|
33
51
|
const path = join(destination, filename);
|
|
34
52
|
// As in multer, `path` is known to the file before it is written.
|
|
35
53
|
file.path = path;
|
|
36
54
|
const out = createWriteStream(path);
|
|
37
55
|
try {
|
|
38
|
-
await
|
|
56
|
+
await Promise.race([
|
|
57
|
+
pipeline(file.stream, out),
|
|
58
|
+
rejectWhenCutOff(file.stream),
|
|
59
|
+
]);
|
|
39
60
|
}
|
|
40
61
|
catch (err) {
|
|
62
|
+
// Removed once closed: a file still being opened would be created
|
|
63
|
+
// after the unlink.
|
|
64
|
+
out.destroy();
|
|
65
|
+
await finished(out).catch(() => undefined);
|
|
41
66
|
await unlink(path).catch(() => undefined);
|
|
42
67
|
throw err;
|
|
43
68
|
}
|
|
@@ -8,12 +8,20 @@ export function memoryStorage() {
|
|
|
8
8
|
return {
|
|
9
9
|
_handleFile(_req, file, callback) {
|
|
10
10
|
const chunks = [];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const onData = (chunk) => chunks.push(chunk);
|
|
12
|
+
const onError = (err) => {
|
|
13
|
+
stopListening();
|
|
14
|
+
callback(err);
|
|
15
|
+
};
|
|
16
|
+
const onEnd = () => {
|
|
17
|
+
stopListening();
|
|
14
18
|
const buffer = Buffer.concat(chunks);
|
|
15
19
|
callback(null, { buffer, size: buffer.length });
|
|
16
|
-
}
|
|
20
|
+
};
|
|
21
|
+
// `@fastify/multipart` keeps every part's stream until the request
|
|
22
|
+
// ends, and with it these listeners and the chunks they hold.
|
|
23
|
+
const stopListening = () => file.stream.off('data', onData).off('error', onError).off('end', onEnd);
|
|
24
|
+
file.stream.on('data', onData).once('error', onError).once('end', onEnd);
|
|
17
25
|
},
|
|
18
26
|
_removeFile(_req, file, callback) {
|
|
19
27
|
delete file.buffer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs/platform-fastify",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.2.0",
|
|
4
4
|
"description": "Nest - modern, fast, powerful node.js web framework (@platform-fastify)",
|
|
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
|
"./multipart": "./multipart/index.js",
|
|
12
13
|
"./*.js": "./*.js",
|
|
13
14
|
"./*": "./*.js"
|
|
@@ -30,9 +31,9 @@
|
|
|
30
31
|
"@fastify/formbody": "9.0.1",
|
|
31
32
|
"@fastify/middie": "9.3.5",
|
|
32
33
|
"fast-querystring": "1.1.2",
|
|
33
|
-
"fastify": "5.12.
|
|
34
|
-
"find-my-way": "9.
|
|
35
|
-
"light-my-request": "6.
|
|
34
|
+
"fastify": "5.12.7",
|
|
35
|
+
"find-my-way": "9.10.0",
|
|
36
|
+
"light-my-request": "6.8.0",
|
|
36
37
|
"path-to-regexp": "8.4.2",
|
|
37
38
|
"tslib": "2.8.1"
|
|
38
39
|
},
|
|
@@ -54,5 +55,5 @@
|
|
|
54
55
|
"optional": true
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "af37111368de7fd2dfab0bdec2f530c7845510c2"
|
|
58
59
|
}
|