fulmine.js 5.19.3 → 5.19.5
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/README.md +9 -4
- package/package.json +1 -1
- package/src/application.js +25 -36
- package/src/cli.js +22 -16
- package/src/cluster.js +2 -2
- package/src/compression.js +102 -61
- package/src/declarative.js +61 -32
- package/src/hot-settings.js +5 -5
- package/src/lazy-readable.js +8 -6
- package/src/lazy-writable.js +3 -3
- package/src/middlewares.js +168 -96
- package/src/nest.js +3 -2
- package/src/node-shim.js +10 -5
- package/src/optimizer.js +9 -7
- package/src/options.d.ts +9 -4
- package/src/request-utils.js +3 -2
- package/src/request.js +49 -38
- package/src/response-utils.js +4 -1
- package/src/response.js +244 -119
- package/src/route.js +3 -3
- package/src/router-utils.js +62 -14
- package/src/router.js +69 -35
- package/src/server-shape.js +14 -10
- package/src/server-timing.js +16 -4
- package/src/socket.js +3 -3
- package/src/testing.js +4 -3
- package/src/usage.js +10 -5
- package/src/utils.js +132 -23
- package/src/verify.js +4 -3
- package/src/view.js +1 -1
- package/src/walk.js +8 -7
- package/src/websocket.js +17 -8
- package/src/work.js +3 -3
package/src/middlewares.js
CHANGED
|
@@ -26,6 +26,7 @@ const mime = require("mime-types");
|
|
|
26
26
|
const compressible = require("compressible");
|
|
27
27
|
const ms = require("ms");
|
|
28
28
|
const qs = require("qs");
|
|
29
|
+
const statuses = require("statuses");
|
|
29
30
|
const parseQuery = require("./parse-query.js");
|
|
30
31
|
const { kGetSafe } = require("./usage.js");
|
|
31
32
|
const { AsyncResource } = require("async_hooks");
|
|
@@ -48,6 +49,7 @@ const {
|
|
|
48
49
|
NullObject,
|
|
49
50
|
asStatError,
|
|
50
51
|
httpError,
|
|
52
|
+
httpErrorName,
|
|
51
53
|
memoizeByString,
|
|
52
54
|
containsDotFile,
|
|
53
55
|
negotiateEncoding,
|
|
@@ -74,6 +76,26 @@ const PRECOMPRESSED = [
|
|
|
74
76
|
|
|
75
77
|
/** @typedef {import("./request.js")} Request */
|
|
76
78
|
/** @typedef {import("./response.js")} Response */
|
|
79
|
+
/** @typedef {import("./utils.js").HttpError} HttpError */
|
|
80
|
+
/** @typedef {import("./options").BodyParserOptions} BodyParserOptions */
|
|
81
|
+
/**
|
|
82
|
+
* A decompressor from fast-zlib, carrying the flag its final flush passes, see createInflate.
|
|
83
|
+
* @typedef {(import("fast-zlib").Inflate|import("fast-zlib").Gunzip|import("fast-zlib").BrotliDecompress)
|
|
84
|
+
* & {_finishFlag?: number}} Inflater
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* What a parser does with the collected bytes: turns them into req.body and carries on. `body` is
|
|
88
|
+
* deliberately not a field of Request, see the comment there, so it is added here. The charset is
|
|
89
|
+
* undefined only for raw, which never decodes: the others settle it before a byte is read.
|
|
90
|
+
* @typedef {(
|
|
91
|
+
* req: Request & {body?: unknown},
|
|
92
|
+
* res: Response,
|
|
93
|
+
* next: (err?: unknown) => void,
|
|
94
|
+
* options: BodyParserOptions,
|
|
95
|
+
* buf: Buffer,
|
|
96
|
+
* encoding: string|undefined
|
|
97
|
+
* ) => void} BodyHandler
|
|
98
|
+
*/
|
|
77
99
|
|
|
78
100
|
// The failures express.static answers by moving on to the next handler rather than by reporting
|
|
79
101
|
// them, when fallthrough is on. They all mean the same thing: the request is not a file here.
|
|
@@ -123,7 +145,7 @@ let iconv;
|
|
|
123
145
|
* iconv-lite, loaded only when a request names a charset the Buffer cannot decode, so the common
|
|
124
146
|
* utf-8 request never pays for it.
|
|
125
147
|
*
|
|
126
|
-
* @returns {
|
|
148
|
+
* @returns {typeof import("iconv-lite")}
|
|
127
149
|
*/
|
|
128
150
|
function loadIconv() {
|
|
129
151
|
if (!iconv) iconv = require("iconv-lite");
|
|
@@ -200,7 +222,7 @@ function decodeBody(buf, encoding) {
|
|
|
200
222
|
case "iso-8859-1":
|
|
201
223
|
return buf.toString("latin1");
|
|
202
224
|
default:
|
|
203
|
-
return loadIconv().decode(buf, encoding);
|
|
225
|
+
return loadIconv().decode(buf, /** @type {import("iconv-lite").Encoding} */ (encoding));
|
|
204
226
|
}
|
|
205
227
|
}
|
|
206
228
|
|
|
@@ -210,29 +232,51 @@ function decodeBody(buf, encoding) {
|
|
|
210
232
|
*
|
|
211
233
|
* @param {Request} req
|
|
212
234
|
* @param {Response} res
|
|
213
|
-
* @param {(err?:
|
|
214
|
-
* @param {
|
|
235
|
+
* @param {(err?: unknown) => void} next
|
|
236
|
+
* @param {BodyParserOptions} options the parser options, settled by createBodyParser
|
|
215
237
|
* @param {Buffer} buf
|
|
238
|
+
* @param {string|undefined} encoding the charset the body is about to be decoded with, undefined
|
|
239
|
+
* for raw, which never decodes
|
|
216
240
|
* @returns {boolean}
|
|
217
241
|
*/
|
|
218
|
-
function runVerify(req, res, next, options, buf) {
|
|
242
|
+
function runVerify(req, res, next, options, buf, encoding) {
|
|
219
243
|
if (!options.verify) {
|
|
220
244
|
return true;
|
|
221
245
|
}
|
|
222
246
|
try {
|
|
223
|
-
|
|
247
|
+
// the charset goes too, as body-parser hands it over: a hook checking a signature over
|
|
248
|
+
// the decoded text needs it. raw gets null, which is what body-parser gives it there
|
|
249
|
+
options.verify(req, res, buf, encoding ?? null);
|
|
224
250
|
return true;
|
|
225
251
|
} catch (e) {
|
|
226
|
-
|
|
227
|
-
next(
|
|
228
|
-
asBodyError(err, err.status ?? err.statusCode ?? 403, err.type ?? "entity.verify.failed", {
|
|
229
|
-
body: buf
|
|
230
|
-
})
|
|
231
|
-
);
|
|
252
|
+
next(verifyError(e, buf));
|
|
232
253
|
return false;
|
|
233
254
|
}
|
|
234
255
|
}
|
|
235
256
|
|
|
257
|
+
/**
|
|
258
|
+
* The error a verify hook's throw becomes, as http-errors shapes it for body-parser. An Error is
|
|
259
|
+
* kept, with its own status when that is one a client can be answered with; a thrown string
|
|
260
|
+
* becomes a 403 with that message, and anything else a plain 403.
|
|
261
|
+
*
|
|
262
|
+
* @param {unknown} thrown
|
|
263
|
+
* @param {Buffer} buf the body, which rides on the error as body-parser puts it there
|
|
264
|
+
* @returns {HttpError}
|
|
265
|
+
*/
|
|
266
|
+
function verifyError(thrown, buf) {
|
|
267
|
+
const own = thrown instanceof Error ? /** @type {HttpError} */ (thrown) : undefined;
|
|
268
|
+
let status = own ? own.status || own.statusCode || 403 : 403;
|
|
269
|
+
// http-errors answers 500 for a status it cannot answer with: not a number, or outside 4xx
|
|
270
|
+
// and 5xx with no message of its own
|
|
271
|
+
if (typeof status !== "number" || (!statuses.message[status] && (status < 400 || status >= 600))) {
|
|
272
|
+
status = 500;
|
|
273
|
+
}
|
|
274
|
+
const err = own ?? httpError(status, typeof thrown === "string" ? thrown : undefined);
|
|
275
|
+
// read off whatever was thrown, as body-parser reads it
|
|
276
|
+
const type = /** @type {{type?: string}|null|undefined} */ (thrown)?.type || "entity.verify.failed";
|
|
277
|
+
return asBodyError(err, status, type, { body: buf });
|
|
278
|
+
}
|
|
279
|
+
|
|
236
280
|
/**
|
|
237
281
|
* The message a strict violation gets, which is the one V8 would have produced had the body been
|
|
238
282
|
* invalid JSON rather than merely not an object.
|
|
@@ -255,22 +299,13 @@ function strictSyntaxMessage(text, char) {
|
|
|
255
299
|
JSON.parse(partial);
|
|
256
300
|
} catch (e) {
|
|
257
301
|
// put the real characters back where the placeholders were named
|
|
258
|
-
return /** @type {
|
|
302
|
+
return /** @type {SyntaxError} */ (e).message.replace(/#+/g, (/** @type {string} */ placeholder) =>
|
|
259
303
|
text.substring(index, index + placeholder.length)
|
|
260
304
|
);
|
|
261
305
|
}
|
|
262
306
|
return "strict violation";
|
|
263
307
|
}
|
|
264
308
|
|
|
265
|
-
// The name http-errors gives each status body-parser answers with. An application reading
|
|
266
|
-
// err.name, or a logger printing it, sees "PayloadTooLargeError" from Express and would have
|
|
267
|
-
// seen a bare "Error" here.
|
|
268
|
-
const BODY_ERROR_NAMES = {
|
|
269
|
-
400: "BadRequestError",
|
|
270
|
-
413: "PayloadTooLargeError",
|
|
271
|
-
415: "UnsupportedMediaTypeError"
|
|
272
|
-
};
|
|
273
|
-
|
|
274
309
|
/**
|
|
275
310
|
* The error a body parser hands to next(), shaped as body-parser shapes it: with a status, since
|
|
276
311
|
* `res.status(err.status || 500)` would otherwise answer 500 to a request that was merely too
|
|
@@ -283,10 +318,11 @@ const BODY_ERROR_NAMES = {
|
|
|
283
318
|
* @returns {Error}
|
|
284
319
|
*/
|
|
285
320
|
function bodyError(message, status, type, extra) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
321
|
+
/** @type {HttpError} */
|
|
322
|
+
const err = new Error(message);
|
|
323
|
+
// the name http-errors gives it: an application reading err.name, or a logger printing it,
|
|
324
|
+
// sees "PayloadTooLargeError" from Express and would have seen a bare "Error" here
|
|
325
|
+
err.name = httpErrorName(status);
|
|
290
326
|
return asBodyError(err, status, type, extra);
|
|
291
327
|
}
|
|
292
328
|
|
|
@@ -296,7 +332,7 @@ function bodyError(message, status, type, extra) {
|
|
|
296
332
|
* its stack and any property the thrower put on it are all still there when the application
|
|
297
333
|
* reads it.
|
|
298
334
|
*
|
|
299
|
-
* @param {
|
|
335
|
+
* @param {HttpError} err the error the fields go on: JSON.parse's SyntaxError, or a verify hook's own
|
|
300
336
|
* @param {number} status
|
|
301
337
|
* @param {string} type body-parser's own name for the kind of failure
|
|
302
338
|
* @param {object} [extra] anything else body-parser puts on that particular error
|
|
@@ -411,7 +447,7 @@ function pickPrecompressed(filePath, accept, ttl, statTtl) {
|
|
|
411
447
|
*
|
|
412
448
|
* @param {string} dir the directory to look in
|
|
413
449
|
* @param {string[]} indexList the index names, in order
|
|
414
|
-
* @returns {{stat:
|
|
450
|
+
* @returns {{stat: import("fs").Stats, name: string, candidate: string}|null} the file to serve, or null
|
|
415
451
|
*/
|
|
416
452
|
function findIndexFile(dir, indexList) {
|
|
417
453
|
let lastError;
|
|
@@ -445,7 +481,7 @@ function findIndexFile(dir, indexList) {
|
|
|
445
481
|
*
|
|
446
482
|
* @param {string} root directory to serve from
|
|
447
483
|
* @param {import("./options").StaticOptions} [options]
|
|
448
|
-
* @returns {(req:
|
|
484
|
+
* @returns {(req: Request, res: Response, next: (err?: unknown) => void) => void}
|
|
449
485
|
*/
|
|
450
486
|
function serveStatic(root, options) {
|
|
451
487
|
// serve-static's own messages, thrown where the middleware is written rather than where a
|
|
@@ -478,21 +514,22 @@ function serveStatic(root, options) {
|
|
|
478
514
|
if (options.setHeaders !== undefined && typeof options.setHeaders !== "function") {
|
|
479
515
|
throw new TypeError("option setHeaders must be function");
|
|
480
516
|
}
|
|
517
|
+
// serve-static's own option, which res.sendFile does not take, so it goes down under a name
|
|
518
|
+
// only this middleware writes, see sendFile
|
|
519
|
+
options._setHeaders = options.setHeaders;
|
|
481
520
|
// How long express.static remembers which twins a path has. A second is short enough that a
|
|
482
521
|
// deploy is picked up while it is still going out, and long enough that the lookup costs
|
|
483
522
|
// nothing under any traffic at all. { cache: false } asks the disk on every request.
|
|
484
523
|
let twinTtl = 0;
|
|
485
524
|
if (options.preCompressed) {
|
|
486
|
-
const cache =
|
|
487
|
-
typeof options.preCompressed === "object" ? options.preCompressed.cache : undefined
|
|
488
|
-
);
|
|
525
|
+
const cache = typeof options.preCompressed === "object" ? options.preCompressed.cache : undefined;
|
|
489
526
|
twinTtl =
|
|
490
527
|
cache === undefined
|
|
491
528
|
? 1000
|
|
492
529
|
: cache === false
|
|
493
530
|
? 0
|
|
494
531
|
: typeof cache === "string"
|
|
495
|
-
? ms(/** @type {
|
|
532
|
+
? ms(/** @type {import("ms").StringValue} */ (cache))
|
|
496
533
|
: cache;
|
|
497
534
|
if (typeof twinTtl !== "number" || !(twinTtl >= 0)) {
|
|
498
535
|
throw new TypeError("option preCompressed.cache must be a duration");
|
|
@@ -662,7 +699,7 @@ function serveStatic(root, options) {
|
|
|
662
699
|
// error handler with its errno, code, syscall and path still on it, and an
|
|
663
700
|
// error handler doing res.send(err) sends those as JSON. Passing the string
|
|
664
701
|
// sent an HTML page instead.
|
|
665
|
-
return next(asStatError(statError));
|
|
702
|
+
return next(asStatError(/** @type {HttpError} */ (statError)));
|
|
666
703
|
} else return next();
|
|
667
704
|
}
|
|
668
705
|
}
|
|
@@ -707,7 +744,7 @@ function serveStatic(root, options) {
|
|
|
707
744
|
res.status(404);
|
|
708
745
|
// the fs error, as above: the index file is missing and the error handler
|
|
709
746
|
// is told which one and where
|
|
710
|
-
return next(asStatError(err));
|
|
747
|
+
return next(asStatError(/** @type {HttpError} */ (err)));
|
|
711
748
|
} else return next();
|
|
712
749
|
}
|
|
713
750
|
if (found === null) {
|
|
@@ -773,12 +810,59 @@ function serveStatic(root, options) {
|
|
|
773
810
|
};
|
|
774
811
|
}
|
|
775
812
|
|
|
813
|
+
/**
|
|
814
|
+
* A zlib throw as the 400 body-parser answers a corrupt body with. zlib reports it twice, and the
|
|
815
|
+
* 'error' a tick later would land on nothing and end the process, so the listener goes back on.
|
|
816
|
+
*
|
|
817
|
+
* @param {Inflater} inflate
|
|
818
|
+
* @param {HttpError} err what inflate.process threw
|
|
819
|
+
* @returns {HttpError} the same error, carrying its status
|
|
820
|
+
*/
|
|
821
|
+
function inflateError(inflate, err) {
|
|
822
|
+
inflate.instance?.on?.("error", () => {});
|
|
823
|
+
err.status = 400;
|
|
824
|
+
err.statusCode = 400;
|
|
825
|
+
err.expose = true;
|
|
826
|
+
return err;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* What a Content-Encoding means here: the decompressor to run, or the 415 it is refused with. An
|
|
831
|
+
* empty body is judged the same way, so both callers share this.
|
|
832
|
+
*
|
|
833
|
+
* @param {string|undefined} rawContentEncoding
|
|
834
|
+
* @param {any} options the parser's options, read loosely: only inflate is looked at
|
|
835
|
+
* @returns {{inflate?: Inflater, error?: HttpError}}
|
|
836
|
+
*/
|
|
837
|
+
function encodingFor(rawContentEncoding, options) {
|
|
838
|
+
if (!options.inflate) {
|
|
839
|
+
const contentEncoding = (rawContentEncoding || "identity").toLowerCase();
|
|
840
|
+
if (contentEncoding !== "identity") {
|
|
841
|
+
return {
|
|
842
|
+
error: bodyError("content encoding unsupported", 415, "encoding.unsupported", {
|
|
843
|
+
encoding: contentEncoding
|
|
844
|
+
})
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return {};
|
|
848
|
+
}
|
|
849
|
+
const inflate = createInflate(rawContentEncoding);
|
|
850
|
+
if (inflate === false) {
|
|
851
|
+
return {
|
|
852
|
+
error: bodyError('unsupported content encoding "' + rawContentEncoding + '"', 415, "encoding.unsupported", {
|
|
853
|
+
encoding: rawContentEncoding
|
|
854
|
+
})
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
return { inflate };
|
|
858
|
+
}
|
|
859
|
+
|
|
776
860
|
/**
|
|
777
861
|
* The decompressor for a Content-Encoding, or undefined when the body is not compressed. An
|
|
778
862
|
* encoding nobody knows throws, since decoding it wrong is worse than refusing.
|
|
779
863
|
*
|
|
780
864
|
* @param {string|undefined} contentEncoding
|
|
781
|
-
* @returns {
|
|
865
|
+
* @returns {Inflater|false|undefined}
|
|
782
866
|
*/
|
|
783
867
|
function createInflate(contentEncoding) {
|
|
784
868
|
const encoding = (contentEncoding || "identity").toLowerCase();
|
|
@@ -799,7 +883,7 @@ function createInflate(contentEncoding) {
|
|
|
799
883
|
return false;
|
|
800
884
|
}
|
|
801
885
|
// the flag the final flush passes, so a truncated stream errors instead of resolving empty
|
|
802
|
-
/** @type {
|
|
886
|
+
/** @type {Inflater} */ (stream)._finishFlag =
|
|
803
887
|
encoding === "br" ? zlib.constants.BROTLI_OPERATION_FINISH : zlib.constants.Z_FINISH;
|
|
804
888
|
return stream;
|
|
805
889
|
}
|
|
@@ -810,9 +894,9 @@ function createInflate(contentEncoding) {
|
|
|
810
894
|
* bytes over. They differ in the content type they claim and in what they turn the bytes into.
|
|
811
895
|
*
|
|
812
896
|
* @param {string} defaultType the type matched when the caller names none
|
|
813
|
-
* @param {
|
|
814
|
-
*
|
|
815
|
-
* @param {(options:
|
|
897
|
+
* @param {BodyHandler} beforeReturn turns the collected bytes into req.body. Called with the
|
|
898
|
+
* request, the response, next, the options, the body and its charset
|
|
899
|
+
* @param {(options: BodyParserOptions) => void} [checkOptions] whatever this parser alone has to check
|
|
816
900
|
* @param {string} [charsetPolicy] which charsets this parser accepts, as body-parser draws the
|
|
817
901
|
* lines: "utf" (json, utf-* only), "urlencoded" (utf-8 and iso-8859-1), "any" (anything iconv
|
|
818
902
|
* knows), or undefined for a parser that never decodes (raw)
|
|
@@ -949,8 +1033,11 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
949
1033
|
}
|
|
950
1034
|
}
|
|
951
1035
|
|
|
952
|
-
//
|
|
953
|
-
// answers 415 even for an empty body, and before the verify hook can run
|
|
1036
|
+
// The charset is settled before anything is read, as body-parser settles it: a bad one
|
|
1037
|
+
// answers 415 even for an empty body, and before the verify hook can run. Its two
|
|
1038
|
+
// halves sit on either side of the encoding, which is the order body-parser reads
|
|
1039
|
+
// them in: the charset this parser accepts at all, then the Content-Encoding, then
|
|
1040
|
+
// whether iconv knows the charset.
|
|
954
1041
|
let encoding;
|
|
955
1042
|
if (charsetPolicy) {
|
|
956
1043
|
encoding = charsetOf(type) ?? defaultCharset;
|
|
@@ -960,9 +1047,16 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
960
1047
|
) {
|
|
961
1048
|
return next(charsetError(encoding));
|
|
962
1049
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const encoded = encodingFor(req._rawHeader("content-encoding"), options);
|
|
1053
|
+
if (encoded.error) {
|
|
1054
|
+
return next(encoded.error);
|
|
1055
|
+
}
|
|
1056
|
+
const inflate = encoded.inflate;
|
|
1057
|
+
|
|
1058
|
+
if (encoding !== undefined && !BUFFER_CHARSETS.has(encoding) && !loadIconv().encodingExists(encoding)) {
|
|
1059
|
+
return next(charsetError(encoding));
|
|
966
1060
|
}
|
|
967
1061
|
|
|
968
1062
|
// an empty body still has to produce this parser's empty value the way express does -
|
|
@@ -971,8 +1065,18 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
971
1065
|
// and the verify hook still runs first: webhook signature checks rely on that
|
|
972
1066
|
if (lengthNumber === 0) {
|
|
973
1067
|
req.bodyRead = true;
|
|
974
|
-
|
|
975
|
-
|
|
1068
|
+
/** @type {Buffer<ArrayBufferLike>} what the parser is handed: zlib's tail is wider */
|
|
1069
|
+
let empty = Buffer.alloc(0);
|
|
1070
|
+
if (inflate) {
|
|
1071
|
+
// nothing to inflate is a stream cut short for zlib, and body-parser answers
|
|
1072
|
+
// that with a 400 rather than with the empty value
|
|
1073
|
+
try {
|
|
1074
|
+
empty = inflate.process(EMPTY_BUFFER, inflate._finishFlag);
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
return next(inflateError(inflate, /** @type {HttpError} */ (e)));
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (!runVerify(req, res, next, options, empty, encoding)) {
|
|
976
1080
|
return;
|
|
977
1081
|
}
|
|
978
1082
|
return beforeReturn(req, res, next, options, empty, encoding);
|
|
@@ -1002,32 +1106,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1002
1106
|
return next();
|
|
1003
1107
|
}
|
|
1004
1108
|
|
|
1005
|
-
let inflate;
|
|
1006
1109
|
let totalSize = 0;
|
|
1007
|
-
const rawContentEncoding = req._rawHeader("content-encoding");
|
|
1008
|
-
const contentEncoding = (rawContentEncoding || "identity").toLowerCase();
|
|
1009
|
-
if (!options.inflate && contentEncoding !== "identity") {
|
|
1010
|
-
return next(
|
|
1011
|
-
bodyError("content encoding unsupported", 415, "encoding.unsupported", {
|
|
1012
|
-
encoding: contentEncoding
|
|
1013
|
-
})
|
|
1014
|
-
);
|
|
1015
|
-
}
|
|
1016
|
-
if (options.inflate) {
|
|
1017
|
-
inflate = createInflate(rawContentEncoding);
|
|
1018
|
-
if (inflate === false) {
|
|
1019
|
-
return next(
|
|
1020
|
-
bodyError(
|
|
1021
|
-
'unsupported content encoding "' + rawContentEncoding + '"',
|
|
1022
|
-
415,
|
|
1023
|
-
"encoding.unsupported",
|
|
1024
|
-
{
|
|
1025
|
-
encoding: rawContentEncoding
|
|
1026
|
-
}
|
|
1027
|
-
)
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
1110
|
|
|
1032
1111
|
// From here the body really gets read, and uWS delivers it on native callbacks that
|
|
1033
1112
|
// carry no async context, so this is the one continuation that has to be bound: an
|
|
@@ -1070,7 +1149,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1070
1149
|
if (copyBody) {
|
|
1071
1150
|
buf = Buffer.from(buf);
|
|
1072
1151
|
}
|
|
1073
|
-
if (!runVerify(req, res, next, options, buf)) {
|
|
1152
|
+
if (!runVerify(req, res, next, options, buf, encoding)) {
|
|
1074
1153
|
return;
|
|
1075
1154
|
}
|
|
1076
1155
|
beforeReturn(req, res, next, options, buf, encoding);
|
|
@@ -1099,24 +1178,16 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1099
1178
|
let finished = false;
|
|
1100
1179
|
|
|
1101
1180
|
/**
|
|
1102
|
-
* A zlib throw becomes the 400 body-parser answers a corrupt body with
|
|
1103
|
-
*
|
|
1104
|
-
* zlib reports it twice: process() throws, and the stream emits 'error' a tick later.
|
|
1105
|
-
* fast-zlib removes its own listeners on the way out, so that second one lands on
|
|
1106
|
-
* nothing, and an unhandled 'error' event ends the process. The listener goes on after
|
|
1107
|
-
* the throw, since process() would have removed it.
|
|
1181
|
+
* A zlib throw becomes the 400 body-parser answers a corrupt body with, and what was
|
|
1182
|
+
* kept of the body goes.
|
|
1108
1183
|
*
|
|
1109
|
-
* @param {
|
|
1184
|
+
* @param {HttpError} err what inflate.process threw
|
|
1110
1185
|
*/
|
|
1111
1186
|
function failInflate(err) {
|
|
1112
|
-
/** @type {any} */ (inflate).instance?.on?.("error", () => {});
|
|
1113
1187
|
finished = true;
|
|
1114
1188
|
abs.length = 0;
|
|
1115
1189
|
target = null;
|
|
1116
|
-
|
|
1117
|
-
err.statusCode = 400;
|
|
1118
|
-
err.expose = true;
|
|
1119
|
-
next(err);
|
|
1190
|
+
next(inflateError(/** @type {Inflater} */ (inflate), err));
|
|
1120
1191
|
}
|
|
1121
1192
|
|
|
1122
1193
|
/**
|
|
@@ -1162,7 +1233,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1162
1233
|
* finished flag matters: uWS goes on delivering chunks after an oversized body has
|
|
1163
1234
|
* been refused, and without it every further chunk would answer the request again.
|
|
1164
1235
|
*
|
|
1165
|
-
* @param {
|
|
1236
|
+
* @param {Buffer|ArrayBuffer} buf a Buffer, or an ArrayBuffer straight from uWS
|
|
1166
1237
|
*/
|
|
1167
1238
|
function onData(buf) {
|
|
1168
1239
|
if (finished) {
|
|
@@ -1177,7 +1248,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1177
1248
|
} catch (e) {
|
|
1178
1249
|
// a body that does not decompress is the client's mistake, and zlib
|
|
1179
1250
|
// throwing here used to escape into whatever called us
|
|
1180
|
-
return failInflate(e);
|
|
1251
|
+
return failInflate(/** @type {HttpError} */ (e));
|
|
1181
1252
|
}
|
|
1182
1253
|
}
|
|
1183
1254
|
|
|
@@ -1198,7 +1269,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1198
1269
|
try {
|
|
1199
1270
|
tail = inflate.process(EMPTY_BUFFER, inflate._finishFlag);
|
|
1200
1271
|
} catch (e) {
|
|
1201
|
-
return failInflate(e);
|
|
1272
|
+
return failInflate(/** @type {HttpError} */ (e));
|
|
1202
1273
|
}
|
|
1203
1274
|
if (tail.length && !keepChunk(tail)) {
|
|
1204
1275
|
return;
|
|
@@ -1226,7 +1297,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1226
1297
|
: abs.length === 1
|
|
1227
1298
|
? abs[0]
|
|
1228
1299
|
: Buffer.concat(abs);
|
|
1229
|
-
if (!runVerify(req, res, next, options, buf)) {
|
|
1300
|
+
if (!runVerify(req, res, next, options, buf, encoding)) {
|
|
1230
1301
|
return;
|
|
1231
1302
|
}
|
|
1232
1303
|
beforeReturn(req, res, next, options, buf, encoding);
|
|
@@ -1274,7 +1345,7 @@ const json = createBodyParser(
|
|
|
1274
1345
|
// either: it decodes through iconv, which strips it, so by the time the first character is
|
|
1275
1346
|
// looked at the mark is gone. JSON.parse would refuse it, so without this a body saved by
|
|
1276
1347
|
// an editor that writes a BOM is answered 400 here and 200 by Express.
|
|
1277
|
-
const text = stripBom(decodeBody(buf, encoding));
|
|
1348
|
+
const text = stripBom(decodeBody(buf, /** @type {string} */ (encoding)));
|
|
1278
1349
|
|
|
1279
1350
|
// "strict" means only an object or an array is a body, which is body-parser's default and
|
|
1280
1351
|
// was not honoured here at all: the check read req.body before this function had parsed
|
|
@@ -1302,7 +1373,7 @@ const json = createBodyParser(
|
|
|
1302
1373
|
} catch (e) {
|
|
1303
1374
|
// V8's own error, which is what body-parser hands on: its message says where the parse
|
|
1304
1375
|
// gave up, and it is still the SyntaxError an application may be testing for
|
|
1305
|
-
const err = /** @type {
|
|
1376
|
+
const err = /** @type {SyntaxError} */ (e);
|
|
1306
1377
|
return next(asBodyError(err, 400, "entity.parse.failed", { body: text }));
|
|
1307
1378
|
}
|
|
1308
1379
|
|
|
@@ -1329,7 +1400,7 @@ const text = createBodyParser(
|
|
|
1329
1400
|
"text/plain",
|
|
1330
1401
|
function (req, res, next, options, buf, encoding) {
|
|
1331
1402
|
try {
|
|
1332
|
-
req.body = decodeBody(buf, encoding);
|
|
1403
|
+
req.body = decodeBody(buf, /** @type {string} */ (encoding));
|
|
1333
1404
|
} catch (e) {
|
|
1334
1405
|
return next(e);
|
|
1335
1406
|
}
|
|
@@ -1371,7 +1442,7 @@ const urlencoded = createBodyParser(
|
|
|
1371
1442
|
"application/x-www-form-urlencoded",
|
|
1372
1443
|
function (req, res, next, options, buf, encoding) {
|
|
1373
1444
|
try {
|
|
1374
|
-
const body = decodeBody(buf, encoding);
|
|
1445
|
+
const body = decodeBody(buf, /** @type {string} */ (encoding));
|
|
1375
1446
|
// Express 5 defaults extended to false, so nested keys need opting in
|
|
1376
1447
|
const extended = typeof options.extended !== "undefined" ? options.extended : false;
|
|
1377
1448
|
// qs has to know the charset itself for anything but utf-8, and the sentinel options
|
|
@@ -1387,7 +1458,8 @@ const urlencoded = createBodyParser(
|
|
|
1387
1458
|
}
|
|
1388
1459
|
req.body = parsed;
|
|
1389
1460
|
} else {
|
|
1390
|
-
|
|
1461
|
+
// settled by the check below the parser, so it is a number by the time a body arrives
|
|
1462
|
+
const count = parameterCount(body, /** @type {number} */ (options.parameterLimit));
|
|
1391
1463
|
if (count === undefined) {
|
|
1392
1464
|
return next(bodyError("too many parameters", 413, "parameters.too.many"));
|
|
1393
1465
|
}
|
|
@@ -1401,7 +1473,7 @@ const urlencoded = createBodyParser(
|
|
|
1401
1473
|
arrayLimit: Math.max(100, count + 1),
|
|
1402
1474
|
charsetSentinel: options.charsetSentinel,
|
|
1403
1475
|
interpretNumericEntities: options.interpretNumericEntities,
|
|
1404
|
-
charset: encoding,
|
|
1476
|
+
charset: /** @type {"utf-8"|"iso-8859-1"} */ (encoding),
|
|
1405
1477
|
parameterLimit: options.parameterLimit
|
|
1406
1478
|
};
|
|
1407
1479
|
req.body = needsQs
|
|
@@ -1419,7 +1491,7 @@ const urlencoded = createBodyParser(
|
|
|
1419
1491
|
strictDepth: true,
|
|
1420
1492
|
charsetSentinel: options.charsetSentinel,
|
|
1421
1493
|
interpretNumericEntities: options.interpretNumericEntities,
|
|
1422
|
-
charset: encoding,
|
|
1494
|
+
charset: /** @type {"utf-8"|"iso-8859-1"} */ (encoding),
|
|
1423
1495
|
parameterLimit: options.parameterLimit
|
|
1424
1496
|
})
|
|
1425
1497
|
);
|
package/src/nest.js
CHANGED
|
@@ -47,7 +47,8 @@ const fulmine = require("./index.js");
|
|
|
47
47
|
*/
|
|
48
48
|
class FulmineExpressAdapter extends ExpressAdapter {
|
|
49
49
|
/**
|
|
50
|
-
* @param {
|
|
50
|
+
* @param {import("fulmine.js").FulmineApplication} [instance] an application from `fulmine()`; one is
|
|
51
|
+
* created when omitted
|
|
51
52
|
*/
|
|
52
53
|
constructor(instance) {
|
|
53
54
|
super(instance || fulmine());
|
|
@@ -61,7 +62,7 @@ class FulmineExpressAdapter extends ExpressAdapter {
|
|
|
61
62
|
/**
|
|
62
63
|
* The app is the server. Nest calls this once, from NestApplication's constructor.
|
|
63
64
|
*
|
|
64
|
-
* @param {
|
|
65
|
+
* @param {import("@nestjs/common").NestApplicationOptions} [options] the options NestFactory.create was given
|
|
65
66
|
* @returns {void}
|
|
66
67
|
*/
|
|
67
68
|
initHttpServer(options) {
|
package/src/node-shim.js
CHANGED
|
@@ -389,7 +389,7 @@ class NodeHttpResponse {
|
|
|
389
389
|
|
|
390
390
|
/**
|
|
391
391
|
* Whether these are node's own request and response rather than this project's.
|
|
392
|
-
* @param {
|
|
392
|
+
* @param {unknown} req anything a caller handed the router, which is the point of the check
|
|
393
393
|
*/
|
|
394
394
|
function isNodeRequest(req) {
|
|
395
395
|
return req instanceof IncomingMessage;
|
|
@@ -398,14 +398,19 @@ function isNodeRequest(req) {
|
|
|
398
398
|
/**
|
|
399
399
|
* Serves a request that arrived through node's HTTP server with the given router or app.
|
|
400
400
|
*
|
|
401
|
-
* @param {
|
|
401
|
+
* @param {import("./router.js")} router the router or application serving this request
|
|
402
402
|
* @param {import("http").IncomingMessage} nodeReq
|
|
403
403
|
* @param {import("http").ServerResponse} nodeRes
|
|
404
|
-
* @param {(err?:
|
|
404
|
+
* @param {(err?: unknown) => void} [next] called when nothing in the router answered
|
|
405
405
|
*/
|
|
406
406
|
function serveNodeRequest(router, nodeReq, nodeRes, next) {
|
|
407
|
-
|
|
408
|
-
const
|
|
407
|
+
// the shims stand in for uWS's pair, and the checker is told so once, here
|
|
408
|
+
const shimRes = /** @type {import("uWebSockets.js").HttpResponse} */ (
|
|
409
|
+
/** @type {unknown} */ (new NodeHttpResponse(nodeReq, nodeRes))
|
|
410
|
+
);
|
|
411
|
+
const shimReq = /** @type {import("uWebSockets.js").HttpRequest} */ (
|
|
412
|
+
/** @type {unknown} */ (new NodeHttpRequest(nodeReq))
|
|
413
|
+
);
|
|
409
414
|
const request = router.handleRequest(shimRes, shimReq);
|
|
410
415
|
const response = request.res;
|
|
411
416
|
// the shim's onAborted rides node's own close event, needed on every request here
|
package/src/optimizer.js
CHANGED
|
@@ -19,6 +19,7 @@ limitations under the License.
|
|
|
19
19
|
|
|
20
20
|
/** @typedef {import("./router.js")} Router */
|
|
21
21
|
/** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
|
|
22
|
+
/** @typedef {import("./application.js").Application} Application */
|
|
22
23
|
|
|
23
24
|
const {
|
|
24
25
|
patternToRegex,
|
|
@@ -53,7 +54,7 @@ const {
|
|
|
53
54
|
let Router;
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
|
-
* @param {
|
|
57
|
+
* @param {typeof import("./router.js")} cls the Router class, passed in to keep this module out of its require cycle
|
|
57
58
|
*/
|
|
58
59
|
function useRouterClass(cls) {
|
|
59
60
|
Router = cls;
|
|
@@ -66,8 +67,8 @@ function useRouterClass(cls) {
|
|
|
66
67
|
*
|
|
67
68
|
* @param {Router} router
|
|
68
69
|
* @param {RouteEntry} route
|
|
69
|
-
* @param {
|
|
70
|
-
* @returns {
|
|
70
|
+
* @param {RouteEntry[]} routes every route of this router, in registration order
|
|
71
|
+
* @returns {RouteEntry[]|false} the chain, ending in the route itself
|
|
71
72
|
*/
|
|
72
73
|
function optimizeRoute(router, route, routes) {
|
|
73
74
|
const optimizedPath = [];
|
|
@@ -216,7 +217,8 @@ function optimizeRoute(router, route, routes) {
|
|
|
216
217
|
* routers and carrying their prefix down. Runs once, when the app starts listening, since it
|
|
217
218
|
* needs every route to have been registered first.
|
|
218
219
|
*
|
|
219
|
-
* @param {
|
|
220
|
+
* @param {Router} root the application whose routes are being compiled. A plain router, which has
|
|
221
|
+
* no uwsApp, is left alone
|
|
220
222
|
*/
|
|
221
223
|
function compileOptimizedRoutes(root) {
|
|
222
224
|
if (!root.uwsApp) {
|
|
@@ -376,7 +378,7 @@ function compileOptimizedRoutes(root) {
|
|
|
376
378
|
*
|
|
377
379
|
* @param {Router} router
|
|
378
380
|
* @param {RouteEntry} route
|
|
379
|
-
* @param {
|
|
381
|
+
* @param {RouteEntry[]} optimizedPath the routes to run, in order, ending with this one
|
|
380
382
|
*/
|
|
381
383
|
function registerUwsRoute(router, route, optimizedPath) {
|
|
382
384
|
let method = route.method.toLowerCase();
|
|
@@ -428,7 +430,7 @@ function registerUwsRoute(router, route, optimizedPath) {
|
|
|
428
430
|
// this one
|
|
429
431
|
if (caseGuards !== null && anyGuardHits(caseGuards, req.getUrl())) {
|
|
430
432
|
// an application is what registers native routes, and only it serves
|
|
431
|
-
return /** @type {
|
|
433
|
+
return /** @type {Application} */ (router)._serveGeneric(res, req);
|
|
432
434
|
}
|
|
433
435
|
const request = router.handleRequest(res, req, preset, skipHolder);
|
|
434
436
|
const response = request.res;
|
|
@@ -526,7 +528,7 @@ function registerUwsRoute(router, route, optimizedPath) {
|
|
|
526
528
|
|
|
527
529
|
// the response prototype the route will really run under: its own app's, which sees a
|
|
528
530
|
// method patched there or inherited from a parent app, falling back to the registering app
|
|
529
|
-
const responseProto =
|
|
531
|
+
const responseProto = route.owner?.response ?? /** @type {Application} */ (router).response;
|
|
530
532
|
// check if route is declarative
|
|
531
533
|
if (
|
|
532
534
|
optimizedPath.length === 1 && // must not have middlewares
|