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