@nestjs/platform-fastify 12.1.1 → 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 +191 -25
- package/adapters/utils/get-media-type-version.util.d.ts +12 -0
- package/adapters/utils/get-media-type-version.util.js +26 -0
- 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"
|
|
@@ -8,6 +8,7 @@ import middie from '@fastify/middie';
|
|
|
8
8
|
import { loadPackage, tryLoadPackage, isNil, isString, isUndefined, } from '@nestjs/common/internal';
|
|
9
9
|
import { AbstractHttpAdapter } from '@nestjs/core';
|
|
10
10
|
import { LegacyRouteConverter } from '@nestjs/core/internal';
|
|
11
|
+
import { getMediaTypeVersion } from './utils/get-media-type-version.util.js';
|
|
11
12
|
const { kRouteContext } = fastifySymbols;
|
|
12
13
|
// Fastify uses `fast-querystring` internally to quickly parse URL query strings.
|
|
13
14
|
import { parse as querystringParse } from 'fast-querystring';
|
|
@@ -22,6 +23,12 @@ function isFastifyMultipartPlugin(plugin) {
|
|
|
22
23
|
(fn[Symbol.for('plugin-meta')]?.name === '@fastify/multipart' ||
|
|
23
24
|
fn.name === 'fastifyMultipart'));
|
|
24
25
|
}
|
|
26
|
+
// Covers "application/json" and the "+json" structured syntax suffix (RFC 6839),
|
|
27
|
+
// e.g. "application/problem+json", with or without parameters.
|
|
28
|
+
function isJsonContentType(contentType) {
|
|
29
|
+
const mediaType = contentType.split(';')[0].trim().toLowerCase();
|
|
30
|
+
return (mediaType.startsWith('application/json') || mediaType.endsWith('+json'));
|
|
31
|
+
}
|
|
25
32
|
/**
|
|
26
33
|
* @publicApi
|
|
27
34
|
*/
|
|
@@ -30,7 +37,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
30
37
|
_pathPrefix;
|
|
31
38
|
openConnections = new Set();
|
|
32
39
|
isClosing = false;
|
|
40
|
+
isShuttingDown = false;
|
|
41
|
+
return503OnClosing = false;
|
|
33
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();
|
|
34
46
|
onRequestHook;
|
|
35
47
|
onResponseHook;
|
|
36
48
|
isMiddieRegistered;
|
|
@@ -79,13 +91,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
79
91
|
// Media Type (Accept Header) Versioning Handler
|
|
80
92
|
if (this.versioningOptions?.type === VersioningType.MEDIA_TYPE) {
|
|
81
93
|
const MEDIA_TYPE_HEADER = 'Accept';
|
|
82
|
-
const acceptHeaderValue =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return isUndefined(acceptHeaderVersionParameter)
|
|
94
|
+
const acceptHeaderValue = req.headers?.[MEDIA_TYPE_HEADER] ||
|
|
95
|
+
req.headers?.[MEDIA_TYPE_HEADER.toLowerCase()];
|
|
96
|
+
const headerVersion = getMediaTypeVersion(acceptHeaderValue, this.versioningOptions.key);
|
|
97
|
+
return isUndefined(headerVersion)
|
|
87
98
|
? VERSION_NEUTRAL // No version was supplied
|
|
88
|
-
:
|
|
99
|
+
: headerVersion;
|
|
89
100
|
}
|
|
90
101
|
// Header Versioning Handler
|
|
91
102
|
else if (this.versioningOptions?.type === VersioningType.HEADER) {
|
|
@@ -116,6 +127,9 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
116
127
|
...this.getTopLevelRouterOptions(instanceOrOptions),
|
|
117
128
|
...instanceOrOptions?.routerOptions,
|
|
118
129
|
constraints: {
|
|
130
|
+
...instanceOrOptions?.constraints,
|
|
131
|
+
...instanceOrOptions?.routerOptions
|
|
132
|
+
?.constraints,
|
|
119
133
|
version: this.versionConstraint,
|
|
120
134
|
},
|
|
121
135
|
},
|
|
@@ -135,7 +149,28 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
135
149
|
}
|
|
136
150
|
this.instance.addHook('onRequest', (request, reply, done) => {
|
|
137
151
|
if (this.onRequestHook) {
|
|
138
|
-
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
|
+
});
|
|
139
174
|
}
|
|
140
175
|
else {
|
|
141
176
|
done();
|
|
@@ -143,13 +178,33 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
143
178
|
});
|
|
144
179
|
this.instance.addHook('onResponse', (request, reply, done) => {
|
|
145
180
|
if (this.onResponseHook) {
|
|
146
|
-
this.onResponseHook
|
|
181
|
+
this.runLifecycleHook(this.onResponseHook, request, reply, done);
|
|
147
182
|
}
|
|
148
183
|
else {
|
|
149
184
|
done();
|
|
150
185
|
}
|
|
151
186
|
});
|
|
152
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
|
+
}
|
|
153
208
|
setOnRequestHook(hook) {
|
|
154
209
|
this.onRequestHook = hook;
|
|
155
210
|
}
|
|
@@ -175,6 +230,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
175
230
|
listen(listenOptions, ...args) {
|
|
176
231
|
const isFirstArgTypeofFunction = typeof args[0] === 'function';
|
|
177
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
|
+
}
|
|
178
238
|
let options;
|
|
179
239
|
if (typeof listenOptions === 'object' &&
|
|
180
240
|
(listenOptions.host !== undefined ||
|
|
@@ -191,8 +251,23 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
191
251
|
if (!isFirstArgTypeofFunction) {
|
|
192
252
|
options.host = args[0];
|
|
193
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
|
+
}
|
|
194
262
|
return this.instance.listen(options, callback);
|
|
195
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
|
+
}
|
|
196
271
|
get(...args) {
|
|
197
272
|
return this.injectRouteOptions('GET', ...args);
|
|
198
273
|
}
|
|
@@ -241,6 +316,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
241
316
|
unlock(...args) {
|
|
242
317
|
return this.injectRouteOptions('UNLOCK', ...args);
|
|
243
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
|
+
}
|
|
244
324
|
applyVersionFilter(handler, version, versioningOptions) {
|
|
245
325
|
if (!this.versioningOptions) {
|
|
246
326
|
this.versioningOptions = versioningOptions;
|
|
@@ -282,7 +362,7 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
282
362
|
}
|
|
283
363
|
const responseContentType = fastifyReply.getHeader('Content-Type');
|
|
284
364
|
if (typeof responseContentType === 'string' &&
|
|
285
|
-
!responseContentType
|
|
365
|
+
!isJsonContentType(responseContentType) &&
|
|
286
366
|
body?.statusCode >= HttpStatus.BAD_REQUEST) {
|
|
287
367
|
Logger.warn("Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses", FastifyAdapter.name);
|
|
288
368
|
fastifyReply.header('Content-Type', 'application/json');
|
|
@@ -297,7 +377,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
297
377
|
return response.code(statusCode);
|
|
298
378
|
}
|
|
299
379
|
end(response, message) {
|
|
300
|
-
|
|
380
|
+
if (this.isNativeResponse(response)) {
|
|
381
|
+
response.end(message);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const reply = response;
|
|
385
|
+
reply.raw.end(message);
|
|
301
386
|
}
|
|
302
387
|
render(response, view, options) {
|
|
303
388
|
return response && response.view(view, options);
|
|
@@ -373,7 +458,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
373
458
|
inject(opts) {
|
|
374
459
|
return this.instance.inject(opts);
|
|
375
460
|
}
|
|
461
|
+
beforeClose() {
|
|
462
|
+
this.isShuttingDown = true;
|
|
463
|
+
}
|
|
376
464
|
async close() {
|
|
465
|
+
this.isShuttingDown = true;
|
|
377
466
|
this.isClosing = true;
|
|
378
467
|
try {
|
|
379
468
|
this.closeOpenConnections();
|
|
@@ -389,6 +478,7 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
389
478
|
}
|
|
390
479
|
initHttpServer(options = {}) {
|
|
391
480
|
this.httpServer = this.instance.server;
|
|
481
|
+
this.return503OnClosing = !!options?.return503OnClosing;
|
|
392
482
|
if (options?.forceCloseConnections) {
|
|
393
483
|
this.trackOpenConnections();
|
|
394
484
|
}
|
|
@@ -409,7 +499,13 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
409
499
|
return this.register(loadPackage('@fastify/view', 'FastifyAdapter.setViewEngine()', () => import('@fastify/view')), options);
|
|
410
500
|
}
|
|
411
501
|
isHeadersSent(response) {
|
|
412
|
-
|
|
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;
|
|
413
509
|
}
|
|
414
510
|
getHeader(response, name) {
|
|
415
511
|
return response.getHeader(name);
|
|
@@ -472,23 +568,29 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
472
568
|
: undefined;
|
|
473
569
|
}
|
|
474
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
|
+
}
|
|
475
578
|
const parserOptions = {
|
|
476
579
|
...(options || {}),
|
|
477
580
|
parseAs: 'buffer',
|
|
478
581
|
};
|
|
582
|
+
const bodyParser = parser ?? this.getDefaultBodyParser(type);
|
|
479
583
|
this.getInstance().addContentTypeParser(type, parserOptions, (req, body, done) => {
|
|
480
584
|
if (rawBody === true && Buffer.isBuffer(body)) {
|
|
481
585
|
req.rawBody = body;
|
|
482
586
|
}
|
|
483
|
-
if (
|
|
484
|
-
|
|
587
|
+
if (bodyParser) {
|
|
588
|
+
bodyParser(req, body, done);
|
|
485
589
|
return;
|
|
486
590
|
}
|
|
487
591
|
done(null, body);
|
|
488
592
|
});
|
|
489
|
-
|
|
490
|
-
// body parser, we mark the parsers as registered.
|
|
491
|
-
this._isParserRegistered = true;
|
|
593
|
+
this.registeredContentTypes.add(this.normalizeContentType(type));
|
|
492
594
|
}
|
|
493
595
|
async createMiddlewareFactory(requestMethod) {
|
|
494
596
|
if (!this.isMiddieRegistered) {
|
|
@@ -576,21 +678,56 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
576
678
|
}
|
|
577
679
|
registerJsonContentParser(rawBody) {
|
|
578
680
|
const contentType = 'application/json';
|
|
681
|
+
if (this.hasCustomBodyParser(contentType)) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
579
684
|
const withRawBody = !!rawBody;
|
|
580
685
|
const { bodyLimit } = this.getInstance().initialConfig;
|
|
581
|
-
this.useBodyParser(contentType, withRawBody, { bodyLimit }
|
|
582
|
-
const { onProtoPoisoning, onConstructorPoisoning } = this.instance.initialConfig;
|
|
583
|
-
const defaultJsonParser = this.instance.getDefaultJsonParser(onProtoPoisoning || 'error', onConstructorPoisoning || 'error');
|
|
584
|
-
defaultJsonParser(req, body, done);
|
|
585
|
-
});
|
|
686
|
+
this.useBodyParser(contentType, withRawBody, { bodyLimit });
|
|
586
687
|
}
|
|
587
688
|
registerUrlencodedContentParser(rawBody) {
|
|
588
689
|
const contentType = 'application/x-www-form-urlencoded';
|
|
690
|
+
if (this.hasCustomBodyParser(contentType)) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
589
693
|
const withRawBody = !!rawBody;
|
|
590
694
|
const { bodyLimit } = this.getInstance().initialConfig;
|
|
591
|
-
this.useBodyParser(contentType, withRawBody, { bodyLimit }
|
|
592
|
-
|
|
593
|
-
|
|
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
|
+
}
|
|
594
731
|
}
|
|
595
732
|
async registerMiddie() {
|
|
596
733
|
this.isMiddieRegistered = true;
|
|
@@ -612,7 +749,7 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
612
749
|
const routeToInject = {
|
|
613
750
|
method: routerMethodKey,
|
|
614
751
|
url: args[0],
|
|
615
|
-
handler: handlerRef,
|
|
752
|
+
handler: this.requireNonEmptyParams(args[0], handlerRef),
|
|
616
753
|
};
|
|
617
754
|
if (!this.instance.supportedMethods.includes(routerMethodKey)) {
|
|
618
755
|
this.instance.addHttpMethod(routerMethodKey, { hasBody: true });
|
|
@@ -643,6 +780,35 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
643
780
|
}
|
|
644
781
|
return this.instance.route(routeToInject);
|
|
645
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
|
+
}
|
|
646
812
|
/**
|
|
647
813
|
* Fastify still accepts the router options ("ignoreTrailingSlash",
|
|
648
814
|
* "caseSensitive", ...) at the top level, but "initialConfig.routerOptions"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the version carried by an `Accept` header for media type versioning,
|
|
3
|
+
* e.g. `2` for `application/json;v=2` with the `v=` key.
|
|
4
|
+
*
|
|
5
|
+
* The version parameter may follow other parameters and may belong to any
|
|
6
|
+
* media range of a list (`text/html, application/json;v=2`); the first match
|
|
7
|
+
* wins. Returns `undefined` when no parameter starts with the key.
|
|
8
|
+
*
|
|
9
|
+
* Duplicated in each platform package so that a platform package never
|
|
10
|
+
* depends on a newer `@nestjs/core` than the one installed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getMediaTypeVersion(acceptHeader: string | string[] | undefined, key: string): string | undefined;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the version carried by an `Accept` header for media type versioning,
|
|
3
|
+
* e.g. `2` for `application/json;v=2` with the `v=` key.
|
|
4
|
+
*
|
|
5
|
+
* The version parameter may follow other parameters and may belong to any
|
|
6
|
+
* media range of a list (`text/html, application/json;v=2`); the first match
|
|
7
|
+
* wins. Returns `undefined` when no parameter starts with the key.
|
|
8
|
+
*
|
|
9
|
+
* Duplicated in each platform package so that a platform package never
|
|
10
|
+
* depends on a newer `@nestjs/core` than the one installed.
|
|
11
|
+
*/
|
|
12
|
+
export function getMediaTypeVersion(acceptHeader, key) {
|
|
13
|
+
const mediaRanges = []
|
|
14
|
+
.concat(acceptHeader ?? [])
|
|
15
|
+
.flatMap(value => value.split(','));
|
|
16
|
+
for (const mediaRange of mediaRanges) {
|
|
17
|
+
const [, ...parameters] = mediaRange.split(';');
|
|
18
|
+
for (const parameter of parameters) {
|
|
19
|
+
const trimmedParameter = parameter.trim();
|
|
20
|
+
if (trimmedParameter.startsWith(key)) {
|
|
21
|
+
return trimmedParameter.slice(key.length);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
@@ -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
|
}
|