@nestjs/platform-fastify 12.1.2 → 12.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/fastify-adapter.d.ts +58 -11
- package/adapters/fastify-adapter.js +430 -53
- 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 +19 -5
- package/multipart/multipart/append-field.util.d.ts +3 -2
- package/multipart/multipart/append-field.util.js +18 -7
- package/multipart/multipart/multipart-interceptor.factory.js +20 -4
- package/multipart/multipart/multipart.constants.d.ts +14 -0
- package/multipart/multipart/multipart.constants.js +14 -0
- package/multipart/multipart/multipart.parser.d.ts +6 -4
- package/multipart/multipart/multipart.parser.js +57 -11
- 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 +9 -8
|
@@ -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';
|
|
@@ -37,15 +39,26 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
37
39
|
_pathPrefix;
|
|
38
40
|
openConnections = new Set();
|
|
39
41
|
isClosing = false;
|
|
42
|
+
isShuttingDown = false;
|
|
43
|
+
return503OnClosing = false;
|
|
40
44
|
_isParserRegistered;
|
|
45
|
+
// Fastify's `hasContentTypeParser('application/json')` is always true (built-in
|
|
46
|
+
// parser), so the types registered through `useBodyParser()` are tracked here.
|
|
47
|
+
registeredContentTypes = new Set();
|
|
41
48
|
onRequestHook;
|
|
42
49
|
onResponseHook;
|
|
50
|
+
securityHook;
|
|
43
51
|
isMiddieRegistered;
|
|
44
52
|
multipartMode = 'auto';
|
|
45
53
|
multipartOptions = {};
|
|
46
54
|
isMultipartRequested = false;
|
|
47
55
|
pendingMiddlewares = [];
|
|
56
|
+
// The encapsulated contexts created since middie was registered (see
|
|
57
|
+
// `registerMiddie()`)
|
|
58
|
+
middieContexts = [];
|
|
48
59
|
versioningOptions;
|
|
60
|
+
// Exceptions thrown by the extractor of custom versioning, by raw request
|
|
61
|
+
versionExtractorErrors = new WeakMap();
|
|
49
62
|
versionConstraint = {
|
|
50
63
|
name: 'version',
|
|
51
64
|
validate(value) {
|
|
@@ -103,7 +116,16 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
103
116
|
}
|
|
104
117
|
// Custom Versioning Handler
|
|
105
118
|
else if (this.versioningOptions?.type === VersioningType.CUSTOM) {
|
|
106
|
-
|
|
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
|
+
}
|
|
107
129
|
}
|
|
108
130
|
return undefined;
|
|
109
131
|
},
|
|
@@ -117,11 +139,28 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
117
139
|
const instance = instanceOrOptions && instanceOrOptions.server
|
|
118
140
|
? instanceOrOptions
|
|
119
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
|
+
},
|
|
120
156
|
...instanceOrOptions,
|
|
121
157
|
routerOptions: {
|
|
122
158
|
...this.getTopLevelRouterOptions(instanceOrOptions),
|
|
123
159
|
...instanceOrOptions?.routerOptions,
|
|
124
160
|
constraints: {
|
|
161
|
+
...instanceOrOptions?.constraints,
|
|
162
|
+
...instanceOrOptions?.routerOptions
|
|
163
|
+
?.constraints,
|
|
125
164
|
version: this.versionConstraint,
|
|
126
165
|
},
|
|
127
166
|
},
|
|
@@ -141,21 +180,102 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
141
180
|
}
|
|
142
181
|
this.instance.addHook('onRequest', (request, reply, done) => {
|
|
143
182
|
if (this.onRequestHook) {
|
|
144
|
-
this.onRequestHook
|
|
183
|
+
this.runLifecycleHook(this.onRequestHook, request, reply, done);
|
|
145
184
|
}
|
|
146
185
|
else {
|
|
147
186
|
done();
|
|
148
187
|
}
|
|
149
188
|
});
|
|
189
|
+
// Fastify's own `return503OnClosing` only applies once `close()` runs,
|
|
190
|
+
// which is after the shutdown hooks. Added here rather than in
|
|
191
|
+
// `initHttpServer()` so that it runs before Nest middleware:
|
|
192
|
+
// `NestFactory.create()` calls `init()`, which registers middie, first.
|
|
193
|
+
// Answers with the same body as Fastify's own 503, so that clients get
|
|
194
|
+
// one response shape for the whole shutdown.
|
|
195
|
+
this.instance.addHook('onRequest', (request, reply, done) => {
|
|
196
|
+
if (this.return503OnClosing && this.isShuttingDown) {
|
|
197
|
+
if (request.raw.httpVersionMajor !== 2) {
|
|
198
|
+
reply.header('Connection', 'close');
|
|
199
|
+
}
|
|
200
|
+
reply.code(503).send({
|
|
201
|
+
error: 'Service Unavailable',
|
|
202
|
+
message: 'Service Unavailable',
|
|
203
|
+
statusCode: 503,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
done();
|
|
208
|
+
}
|
|
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
|
+
});
|
|
150
250
|
this.instance.addHook('onResponse', (request, reply, done) => {
|
|
151
251
|
if (this.onResponseHook) {
|
|
152
|
-
this.onResponseHook
|
|
252
|
+
this.runLifecycleHook(this.onResponseHook, request, reply, done);
|
|
153
253
|
}
|
|
154
254
|
else {
|
|
155
255
|
done();
|
|
156
256
|
}
|
|
157
257
|
});
|
|
158
258
|
}
|
|
259
|
+
runLifecycleHook(hook, request, reply, done) {
|
|
260
|
+
let completed = false;
|
|
261
|
+
const doneOnce = (err) => {
|
|
262
|
+
if (completed)
|
|
263
|
+
return;
|
|
264
|
+
completed = true;
|
|
265
|
+
done(err);
|
|
266
|
+
};
|
|
267
|
+
try {
|
|
268
|
+
// Do not return the promise to Fastify: both completion styles share
|
|
269
|
+
// this guard, including hooks that call done and return a promise.
|
|
270
|
+
const result = hook.call(this, request, reply, doneOnce);
|
|
271
|
+
if (result && typeof result.then === 'function') {
|
|
272
|
+
result.then(() => doneOnce(), err => doneOnce(err || new fastify.errorCodes.FST_ERR_SEND_UNDEFINED_ERR()));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
doneOnce(err);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
159
279
|
setOnRequestHook(hook) {
|
|
160
280
|
this.onRequestHook = hook;
|
|
161
281
|
}
|
|
@@ -181,6 +301,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
181
301
|
listen(listenOptions, ...args) {
|
|
182
302
|
const isFirstArgTypeofFunction = typeof args[0] === 'function';
|
|
183
303
|
const callback = isFirstArgTypeofFunction ? args[0] : args[1];
|
|
304
|
+
if (typeof listenOptions === 'string' && Number.isNaN(+listenOptions)) {
|
|
305
|
+
// A string that is not a number is a Unix socket path or a named pipe.
|
|
306
|
+
// The host does not apply to it.
|
|
307
|
+
return this.instance.listen({ path: listenOptions }, callback);
|
|
308
|
+
}
|
|
184
309
|
let options;
|
|
185
310
|
if (typeof listenOptions === 'object' &&
|
|
186
311
|
(listenOptions.host !== undefined ||
|
|
@@ -197,8 +322,23 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
197
322
|
if (!isFirstArgTypeofFunction) {
|
|
198
323
|
options.host = args[0];
|
|
199
324
|
}
|
|
325
|
+
if (options.port !== undefined && !this.isValidPort(options.port)) {
|
|
326
|
+
// Fastify calls `server.listen()` from its ready callback, where the
|
|
327
|
+
// RangeError Node throws for a bad port becomes an unhandled rejection
|
|
328
|
+
// and the callback never runs. Throw right away instead, as the
|
|
329
|
+
// `http.Server` of the Express adapter does.
|
|
330
|
+
const error = new RangeError(`Options.port should be >= 0 and < 65536. Received ${String(options.port)}.`);
|
|
331
|
+
throw Object.assign(error, { code: 'ERR_SOCKET_BAD_PORT' });
|
|
332
|
+
}
|
|
200
333
|
return this.instance.listen(options, callback);
|
|
201
334
|
}
|
|
335
|
+
isValidPort(port) {
|
|
336
|
+
const value = typeof port === 'string' && port.trim() !== '' ? Number(port) : port;
|
|
337
|
+
return (typeof value === 'number' &&
|
|
338
|
+
Number.isInteger(value) &&
|
|
339
|
+
value >= 0 &&
|
|
340
|
+
value <= 65535);
|
|
341
|
+
}
|
|
202
342
|
get(...args) {
|
|
203
343
|
return this.injectRouteOptions('GET', ...args);
|
|
204
344
|
}
|
|
@@ -247,6 +387,16 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
247
387
|
unlock(...args) {
|
|
248
388
|
return this.injectRouteOptions('UNLOCK', ...args);
|
|
249
389
|
}
|
|
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
|
+
}
|
|
396
|
+
const handlerRef = args[args.length - 1];
|
|
397
|
+
args[args.length - 1] = this.requireNonEmptyParams(args[0], handlerRef);
|
|
398
|
+
return this.instance.all(...args);
|
|
399
|
+
}
|
|
250
400
|
applyVersionFilter(handler, version, versioningOptions) {
|
|
251
401
|
if (!this.versioningOptions) {
|
|
252
402
|
this.versioningOptions = versioningOptions;
|
|
@@ -271,6 +421,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
271
421
|
fastifyReply.status(statusCode);
|
|
272
422
|
}
|
|
273
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
|
+
}
|
|
274
430
|
const streamHeaders = body.getHeaders();
|
|
275
431
|
if (fastifyReply.getHeader('Content-Type') === undefined &&
|
|
276
432
|
streamHeaders.type !== undefined) {
|
|
@@ -284,7 +440,29 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
284
440
|
streamHeaders.length !== undefined) {
|
|
285
441
|
fastifyReply.header('Content-Length', streamHeaders.length);
|
|
286
442
|
}
|
|
287
|
-
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
|
+
};
|
|
288
466
|
}
|
|
289
467
|
const responseContentType = fastifyReply.getHeader('Content-Type');
|
|
290
468
|
if (typeof responseContentType === 'string' &&
|
|
@@ -303,7 +481,12 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
303
481
|
return response.code(statusCode);
|
|
304
482
|
}
|
|
305
483
|
end(response, message) {
|
|
306
|
-
|
|
484
|
+
if (this.isNativeResponse(response)) {
|
|
485
|
+
response.end(message);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const reply = response;
|
|
489
|
+
reply.raw.end(message);
|
|
307
490
|
}
|
|
308
491
|
render(response, view, options) {
|
|
309
492
|
return response && response.view(view, options);
|
|
@@ -379,7 +562,11 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
379
562
|
inject(opts) {
|
|
380
563
|
return this.instance.inject(opts);
|
|
381
564
|
}
|
|
565
|
+
beforeClose() {
|
|
566
|
+
this.isShuttingDown = true;
|
|
567
|
+
}
|
|
382
568
|
async close() {
|
|
569
|
+
this.isShuttingDown = true;
|
|
383
570
|
this.isClosing = true;
|
|
384
571
|
try {
|
|
385
572
|
this.closeOpenConnections();
|
|
@@ -395,6 +582,7 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
395
582
|
}
|
|
396
583
|
initHttpServer(options = {}) {
|
|
397
584
|
this.httpServer = this.instance.server;
|
|
585
|
+
this.return503OnClosing = !!options?.return503OnClosing;
|
|
398
586
|
if (options?.forceCloseConnections) {
|
|
399
587
|
this.trackOpenConnections();
|
|
400
588
|
}
|
|
@@ -415,26 +603,52 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
415
603
|
return this.register(loadPackage('@fastify/view', 'FastifyAdapter.setViewEngine()', () => import('@fastify/view')), options);
|
|
416
604
|
}
|
|
417
605
|
isHeadersSent(response) {
|
|
418
|
-
|
|
606
|
+
if (this.isNativeResponse(response)) {
|
|
607
|
+
return response.headersSent;
|
|
608
|
+
}
|
|
609
|
+
const reply = response;
|
|
610
|
+
// `reply.sent` only covers hijacked or ended replies, not headers
|
|
611
|
+
// already flushed through `reply.raw`.
|
|
612
|
+
return reply.sent || reply.raw.headersSent;
|
|
419
613
|
}
|
|
420
614
|
getHeader(response, name) {
|
|
421
615
|
return response.getHeader(name);
|
|
422
616
|
}
|
|
423
617
|
setHeader(response, name, value) {
|
|
424
|
-
|
|
618
|
+
if (this.isNativeResponse(response)) {
|
|
619
|
+
return response.setHeader(name, value);
|
|
620
|
+
}
|
|
621
|
+
const reply = response;
|
|
622
|
+
return reply.header(name, value);
|
|
425
623
|
}
|
|
426
624
|
appendHeader(response, name, value) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
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));
|
|
430
628
|
}
|
|
431
|
-
|
|
432
|
-
//
|
|
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.
|
|
433
635
|
if (String(name).toLowerCase() === 'set-cookie') {
|
|
434
|
-
|
|
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);
|
|
435
649
|
}
|
|
436
650
|
const values = Array.isArray(current) ? current : [current];
|
|
437
|
-
return
|
|
651
|
+
return reply.header(name, values.concat(value));
|
|
438
652
|
}
|
|
439
653
|
getRequestHostname(request) {
|
|
440
654
|
return request.hostname;
|
|
@@ -450,19 +664,18 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
450
664
|
}
|
|
451
665
|
/**
|
|
452
666
|
* Runs the request hook of the built-in HTTP security features in an
|
|
453
|
-
* `onRequest` hook: before middie (Nest middleware
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
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
|
|
457
672
|
* `reply.header()` / `@Header()` values take precedence, and responses
|
|
458
|
-
* written to `reply.raw` directly (e.g. `@Sse()
|
|
459
|
-
* 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
|
|
460
675
|
* installed with `setErrorHandler()`.
|
|
461
676
|
*/
|
|
462
677
|
registerSecurityHook(hook) {
|
|
463
|
-
this.
|
|
464
|
-
done(hook(request, reply.raw));
|
|
465
|
-
});
|
|
678
|
+
this.securityHook = hook;
|
|
466
679
|
}
|
|
467
680
|
registerParserMiddleware(prefix, rawBody) {
|
|
468
681
|
if (this._isParserRegistered) {
|
|
@@ -478,23 +691,29 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
478
691
|
: undefined;
|
|
479
692
|
}
|
|
480
693
|
useBodyParser(type, rawBody, options, parser) {
|
|
694
|
+
if (Array.isArray(type)) {
|
|
695
|
+
// One by one, so that each content type gets its own default parser.
|
|
696
|
+
for (const contentType of type) {
|
|
697
|
+
this.useBodyParser(contentType, rawBody, options, parser);
|
|
698
|
+
}
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
481
701
|
const parserOptions = {
|
|
482
702
|
...(options || {}),
|
|
483
703
|
parseAs: 'buffer',
|
|
484
704
|
};
|
|
705
|
+
const bodyParser = parser ?? this.getDefaultBodyParser(type);
|
|
485
706
|
this.getInstance().addContentTypeParser(type, parserOptions, (req, body, done) => {
|
|
486
707
|
if (rawBody === true && Buffer.isBuffer(body)) {
|
|
487
708
|
req.rawBody = body;
|
|
488
709
|
}
|
|
489
|
-
if (
|
|
490
|
-
|
|
710
|
+
if (bodyParser) {
|
|
711
|
+
bodyParser(req, body, done);
|
|
491
712
|
return;
|
|
492
713
|
}
|
|
493
714
|
done(null, body);
|
|
494
715
|
});
|
|
495
|
-
|
|
496
|
-
// body parser, we mark the parsers as registered.
|
|
497
|
-
this._isParserRegistered = true;
|
|
716
|
+
this.registeredContentTypes.add(this.normalizeContentType(type));
|
|
498
717
|
}
|
|
499
718
|
async createMiddlewareFactory(requestMethod) {
|
|
500
719
|
if (!this.isMiddieRegistered) {
|
|
@@ -520,9 +739,13 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
520
739
|
re = hasEndOfStringCharacter
|
|
521
740
|
? new RegExp(re.source + '$', re.flags)
|
|
522
741
|
: re;
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
|
|
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) => {
|
|
526
749
|
const queryParamsIndex = req.originalUrl.indexOf('?');
|
|
527
750
|
let pathname = queryParamsIndex >= 0
|
|
528
751
|
? req.originalUrl.slice(0, queryParamsIndex)
|
|
@@ -535,9 +758,28 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
535
758
|
if (!re.exec(pathToCheck)) {
|
|
536
759
|
return next();
|
|
537
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
|
+
}
|
|
538
770
|
}
|
|
539
|
-
|
|
540
|
-
}
|
|
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
|
+
}
|
|
541
783
|
}
|
|
542
784
|
catch (e) {
|
|
543
785
|
if (e instanceof TypeError) {
|
|
@@ -547,12 +789,53 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
547
789
|
}
|
|
548
790
|
};
|
|
549
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
|
+
}
|
|
550
820
|
getType() {
|
|
551
821
|
return 'fastify';
|
|
552
822
|
}
|
|
553
823
|
isRouteOrderSensitive() {
|
|
554
824
|
return false;
|
|
555
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
|
+
}
|
|
556
839
|
use(...args) {
|
|
557
840
|
// Fastify requires @fastify/middie plugin to be registered before middleware can be used.
|
|
558
841
|
// If middie is not registered yet, we queue the middleware and register it later during init.
|
|
@@ -582,25 +865,69 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
582
865
|
}
|
|
583
866
|
registerJsonContentParser(rawBody) {
|
|
584
867
|
const contentType = 'application/json';
|
|
868
|
+
if (this.hasCustomBodyParser(contentType)) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
585
871
|
const withRawBody = !!rawBody;
|
|
586
872
|
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
|
-
});
|
|
873
|
+
this.useBodyParser(contentType, withRawBody, { bodyLimit });
|
|
592
874
|
}
|
|
593
875
|
registerUrlencodedContentParser(rawBody) {
|
|
594
876
|
const contentType = 'application/x-www-form-urlencoded';
|
|
877
|
+
if (this.hasCustomBodyParser(contentType)) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
595
880
|
const withRawBody = !!rawBody;
|
|
596
881
|
const { bodyLimit } = this.getInstance().initialConfig;
|
|
597
|
-
this.useBodyParser(contentType, withRawBody, { bodyLimit }
|
|
598
|
-
|
|
599
|
-
|
|
882
|
+
this.useBodyParser(contentType, withRawBody, { bodyLimit });
|
|
883
|
+
}
|
|
884
|
+
// A parser registered through `useBodyParser()` takes precedence over the
|
|
885
|
+
// default one, including a RegExp or catch-all ('*') parser matching the type.
|
|
886
|
+
hasCustomBodyParser(contentType) {
|
|
887
|
+
for (const type of this.registeredContentTypes) {
|
|
888
|
+
if (type === '*' || type === contentType) {
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
if (isString(type)) {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
type.lastIndex = 0;
|
|
895
|
+
if (type.test(contentType)) {
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
// Fastify stores string content types trimmed and lower-cased.
|
|
902
|
+
normalizeContentType(type) {
|
|
903
|
+
return isString(type) ? type.trim().toLowerCase() : type;
|
|
904
|
+
}
|
|
905
|
+
getDefaultBodyParser(type) {
|
|
906
|
+
switch (this.normalizeContentType(type)) {
|
|
907
|
+
case 'application/json':
|
|
908
|
+
return (req, body, done) => {
|
|
909
|
+
const { onProtoPoisoning, onConstructorPoisoning } = this.instance.initialConfig;
|
|
910
|
+
const defaultJsonParser = this.instance.getDefaultJsonParser(onProtoPoisoning || 'error', onConstructorPoisoning || 'error');
|
|
911
|
+
defaultJsonParser(req, body, done);
|
|
912
|
+
};
|
|
913
|
+
case 'application/x-www-form-urlencoded':
|
|
914
|
+
return (_req, body, done) => {
|
|
915
|
+
done(null, querystringParse(body.toString()));
|
|
916
|
+
};
|
|
917
|
+
}
|
|
600
918
|
}
|
|
601
919
|
async registerMiddie() {
|
|
602
920
|
this.isMiddieRegistered = true;
|
|
603
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
|
+
});
|
|
604
931
|
}
|
|
605
932
|
getRequestOriginalUrl(rawRequest) {
|
|
606
933
|
return rawRequest.originalUrl || rawRequest.url;
|
|
@@ -618,9 +945,10 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
618
945
|
const routeToInject = {
|
|
619
946
|
method: routerMethodKey,
|
|
620
947
|
url: args[0],
|
|
621
|
-
handler: handlerRef,
|
|
948
|
+
handler: this.requireNonEmptyParams(args[0], handlerRef),
|
|
622
949
|
};
|
|
623
|
-
if (
|
|
950
|
+
if (isString(routerMethodKey) &&
|
|
951
|
+
!this.instance.supportedMethods.includes(routerMethodKey)) {
|
|
624
952
|
this.instance.addHttpMethod(routerMethodKey, { hasBody: true });
|
|
625
953
|
}
|
|
626
954
|
if (isVersioned || hasConstraints || hasConfig || hasSchema) {
|
|
@@ -649,6 +977,35 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
649
977
|
}
|
|
650
978
|
return this.instance.route(routeToInject);
|
|
651
979
|
}
|
|
980
|
+
/**
|
|
981
|
+
* "find-my-way" matches an empty path segment for a route parameter
|
|
982
|
+
* ("/users//profile" -> { id: "" }), while "path-to-regexp", which
|
|
983
|
+
* middleware paths (and the ExpressAdapter's routes) are matched with,
|
|
984
|
+
* needs at least one character per parameter. Requests with an empty route
|
|
985
|
+
* parameter are passed to the not-found handler instead, as with the
|
|
986
|
+
* ExpressAdapter, so that middleware and routes always match the same
|
|
987
|
+
* requests. Only the wildcard ("*") may be empty.
|
|
988
|
+
*/
|
|
989
|
+
requireNonEmptyParams(url, handler) {
|
|
990
|
+
if (isString(url) && !url.includes(':')) {
|
|
991
|
+
return handler;
|
|
992
|
+
}
|
|
993
|
+
const routeHandler = function (request, reply) {
|
|
994
|
+
const params = request.params;
|
|
995
|
+
for (const name in params) {
|
|
996
|
+
if (params[name] === '' && name !== '*') {
|
|
997
|
+
reply.callNotFound();
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return handler.call(this, request, reply);
|
|
1002
|
+
};
|
|
1003
|
+
Object.defineProperty(routeHandler, 'name', {
|
|
1004
|
+
configurable: true,
|
|
1005
|
+
value: handler.name,
|
|
1006
|
+
});
|
|
1007
|
+
return routeHandler;
|
|
1008
|
+
}
|
|
652
1009
|
/**
|
|
653
1010
|
* Fastify still accepts the router options ("ignoreTrailingSlash",
|
|
654
1011
|
* "caseSensitive", ...) at the top level, but "initialConfig.routerOptions"
|
|
@@ -677,7 +1034,8 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
677
1034
|
}
|
|
678
1035
|
sanitizeUrl(url) {
|
|
679
1036
|
const initialConfig = this.instance.initialConfig;
|
|
680
|
-
const routerOptions = initialConfig.routerOptions
|
|
1037
|
+
const routerOptions = (initialConfig.routerOptions ??
|
|
1038
|
+
{});
|
|
681
1039
|
// Absolute-form request targets ("GET http://host/path HTTP/1.1") must be
|
|
682
1040
|
// resolved to their path before any other normalization, as the Fastify
|
|
683
1041
|
// router does, so that middleware and routes always match the same path.
|
|
@@ -686,16 +1044,15 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
686
1044
|
initialConfig.ignoreDuplicateSlashes) {
|
|
687
1045
|
url = this.removeDuplicateSlashes(url);
|
|
688
1046
|
}
|
|
689
|
-
|
|
690
|
-
|
|
1047
|
+
const { caseSensitive, ignoreTrailingSlash } = this.getRouteMatchingOptions();
|
|
1048
|
+
if (ignoreTrailingSlash) {
|
|
691
1049
|
url = this.trimLastSlash(url);
|
|
692
1050
|
}
|
|
693
|
-
|
|
694
|
-
initialConfig.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
return
|
|
698
|
-
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();
|
|
699
1056
|
}
|
|
700
1057
|
removeDuplicateSlashes(path) {
|
|
701
1058
|
const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
|
|
@@ -734,6 +1091,26 @@ export class FastifyAdapter extends AbstractHttpAdapter {
|
|
|
734
1091
|
}
|
|
735
1092
|
return pathStart === -1 ? '/' : url.slice(pathStart);
|
|
736
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
|
+
}
|
|
737
1114
|
trackOpenConnections() {
|
|
738
1115
|
const track = (socket) => {
|
|
739
1116
|
if (this.isClosing) {
|