fulmine.js 5.19.2 → 5.19.4
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/package.json +2 -1
- package/src/adopt.js +20 -26
- package/src/application.js +63 -73
- package/src/cli.js +48 -48
- package/src/cluster.js +18 -27
- package/src/compression.js +141 -112
- package/src/declarative.js +611 -540
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +131 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +153 -130
- package/src/nest.js +22 -36
- package/src/node-shim.js +19 -16
- package/src/optimizer.js +600 -0
- package/src/options.d.ts +9 -4
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +307 -0
- package/src/request.js +147 -548
- package/src/response-utils.js +88 -0
- package/src/response.js +228 -535
- package/src/route.js +7 -8
- package/src/router-utils.js +998 -0
- package/src/router.js +166 -2170
- package/src/server-shape.js +40 -51
- package/src/server-timing.js +32 -33
- package/src/socket.js +208 -0
- package/src/testing.js +43 -45
- package/src/usage.js +25 -25
- package/src/utils.js +165 -78
- package/src/verify.js +22 -31
- package/src/view.js +6 -8
- package/src/walk.js +581 -0
- package/src/websocket.js +34 -26
- package/src/work.js +22 -28
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,
|
|
@@ -72,17 +74,38 @@ const PRECOMPRESSED = [
|
|
|
72
74
|
{ encoding: "gzip", suffix: ".gz", flag: ENCODING_GZIP }
|
|
73
75
|
];
|
|
74
76
|
|
|
77
|
+
/** @typedef {import("./request.js")} Request */
|
|
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
|
+
*/
|
|
99
|
+
|
|
75
100
|
// The failures express.static answers by moving on to the next handler rather than by reporting
|
|
76
101
|
// them, when fallthrough is on. They all mean the same thing: the request is not a file here.
|
|
77
102
|
//
|
|
78
|
-
// serve-static decides this by remembering whether send got as far as settling on a file
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// is a dotfile rule or a path that will not decode.
|
|
103
|
+
// serve-static decides this by remembering whether send got as far as settling on a file. The list
|
|
104
|
+
// says the same from the other side: by the time this hands over the file has been found and
|
|
105
|
+
// stat'ed, so what is left to fail is a dotfile rule or a path that will not decode.
|
|
82
106
|
//
|
|
83
|
-
// A 412 and a 416 are not on it
|
|
84
|
-
//
|
|
85
|
-
// Not Satisfiable came back as a 404, which tells the client its file is gone when it is not.
|
|
107
|
+
// A 412 and a 416 are not on it. Both are about a file that exists and about conditions the client
|
|
108
|
+
// itself set, and falling through swallowed them: a Range Not Satisfiable came back as a 404.
|
|
86
109
|
const FALLTHROUGH_STATUSES = new Set([400, 403, 404]);
|
|
87
110
|
|
|
88
111
|
/**
|
|
@@ -122,7 +145,7 @@ let iconv;
|
|
|
122
145
|
* iconv-lite, loaded only when a request names a charset the Buffer cannot decode, so the common
|
|
123
146
|
* utf-8 request never pays for it.
|
|
124
147
|
*
|
|
125
|
-
* @returns {
|
|
148
|
+
* @returns {typeof import("iconv-lite")}
|
|
126
149
|
*/
|
|
127
150
|
function loadIconv() {
|
|
128
151
|
if (!iconv) iconv = require("iconv-lite");
|
|
@@ -199,7 +222,7 @@ function decodeBody(buf, encoding) {
|
|
|
199
222
|
case "iso-8859-1":
|
|
200
223
|
return buf.toString("latin1");
|
|
201
224
|
default:
|
|
202
|
-
return loadIconv().decode(buf, encoding);
|
|
225
|
+
return loadIconv().decode(buf, /** @type {import("iconv-lite").Encoding} */ (encoding));
|
|
203
226
|
}
|
|
204
227
|
}
|
|
205
228
|
|
|
@@ -207,39 +230,60 @@ function decodeBody(buf, encoding) {
|
|
|
207
230
|
* Runs the caller's verify hook the way body-parser does, an empty body included; a throw becomes
|
|
208
231
|
* the 403 entity.verify.failed error. Answers whether parsing may continue.
|
|
209
232
|
*
|
|
210
|
-
* @param {
|
|
211
|
-
* @param {
|
|
212
|
-
* @param {(err?:
|
|
213
|
-
* @param {
|
|
233
|
+
* @param {Request} req
|
|
234
|
+
* @param {Response} res
|
|
235
|
+
* @param {(err?: unknown) => void} next
|
|
236
|
+
* @param {BodyParserOptions} options the parser options, settled by createBodyParser
|
|
214
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
|
|
215
240
|
* @returns {boolean}
|
|
216
241
|
*/
|
|
217
|
-
function runVerify(req, res, next, options, buf) {
|
|
242
|
+
function runVerify(req, res, next, options, buf, encoding) {
|
|
218
243
|
if (!options.verify) {
|
|
219
244
|
return true;
|
|
220
245
|
}
|
|
221
246
|
try {
|
|
222
|
-
|
|
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);
|
|
223
250
|
return true;
|
|
224
251
|
} catch (e) {
|
|
225
|
-
|
|
226
|
-
next(
|
|
227
|
-
asBodyError(err, err.status ?? err.statusCode ?? 403, err.type ?? "entity.verify.failed", {
|
|
228
|
-
body: buf
|
|
229
|
-
})
|
|
230
|
-
);
|
|
252
|
+
next(verifyError(e, buf));
|
|
231
253
|
return false;
|
|
232
254
|
}
|
|
233
255
|
}
|
|
234
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
|
+
|
|
235
280
|
/**
|
|
236
281
|
* The message a strict violation gets, which is the one V8 would have produced had the body been
|
|
237
282
|
* invalid JSON rather than merely not an object.
|
|
238
283
|
*
|
|
239
|
-
* body-parser
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* showing err.message reads the same sentence either way, naming the character and its position.
|
|
284
|
+
* body-parser builds a string that is the body up to the offending character followed by
|
|
285
|
+
* placeholders, asks JSON.parse to fail on that, and puts the real characters back into what V8
|
|
286
|
+
* said, so an application showing err.message reads the same sentence either way.
|
|
243
287
|
*
|
|
244
288
|
* @param {string} text the body as sent
|
|
245
289
|
* @param {string|undefined} char the first character that is neither whitespace nor { nor [
|
|
@@ -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
|
|
@@ -361,12 +397,12 @@ function twinsOf(filePath, ttl) {
|
|
|
361
397
|
* The compressed twin of a file to serve in its place, or undefined when the client would rather
|
|
362
398
|
* have the file itself or the twin is not there.
|
|
363
399
|
*
|
|
364
|
-
* The stat comes back with it, and is what sendFile then answers from: the ETag and
|
|
365
|
-
*
|
|
366
|
-
*
|
|
400
|
+
* The stat comes back with it, and is what sendFile then answers from: the ETag and Last-Modified
|
|
401
|
+
* of a variant are its own. Two bodies sharing one ETag is how a shared cache ends up handing
|
|
402
|
+
* brotli to a client that cannot read it.
|
|
367
403
|
*
|
|
368
|
-
* One stat when the answer is a twin,
|
|
369
|
-
*
|
|
404
|
+
* One stat when the answer is a twin, none when the last request already found there is no twin.
|
|
405
|
+
* See twinCache above for what is remembered.
|
|
370
406
|
*
|
|
371
407
|
* @param {string} filePath absolute path of the file that was asked for
|
|
372
408
|
* @param {string|undefined} accept the request's Accept-Encoding
|
|
@@ -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");
|
|
@@ -555,12 +592,11 @@ function serveStatic(root, options) {
|
|
|
555
592
|
// Joined against the root and not normalised on its own first, which is the difference
|
|
556
593
|
// between "/mount/../package.json" being refused and being served: a ".." has to climb
|
|
557
594
|
// relative to the root so the check below can see it leave, and normalizing the url alone
|
|
558
|
-
// clamps it at "/"
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
// Windows stats it either way, which is why only the CI said so.
|
|
595
|
+
// clamps it at "/". Absolute because resolvedRoot is, so nothing resolves against the
|
|
596
|
+
// working directory per request.
|
|
597
|
+
// Without the trailing separator join keeps and resolve does not, because statTarget below
|
|
598
|
+
// puts it back only where it belongs: linux refuses a file asked for as a directory, so a
|
|
599
|
+
// mount whose root is a file would answer nothing. Windows stats it either way
|
|
564
600
|
let fullpath = path.join(resolvedRoot, url);
|
|
565
601
|
if (fullpath.length > resolvedRoot.length && fullpath.endsWith(path.sep)) {
|
|
566
602
|
fullpath = fullpath.slice(0, -1);
|
|
@@ -570,12 +606,11 @@ function serveStatic(root, options) {
|
|
|
570
606
|
let filePath = fullpath;
|
|
571
607
|
// What serve-static hands send is this path, except that a bare "/" under a mount the
|
|
572
608
|
// request did not write with one becomes "": without that rule a mount whose root is a file
|
|
573
|
-
// would ask the disk for a directory and could never answer
|
|
609
|
+
// would ask the disk for a directory and could never answer.
|
|
574
610
|
//
|
|
575
|
-
// Send then stats `normalize(join(root, path))`, and both
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
// is what an error handler prints when fallthrough is off.
|
|
611
|
+
// Send then stats `normalize(join(root, path))`, and both keep a trailing separator where
|
|
612
|
+
// `resolve` takes it off. The disk refuses a file asked for as a directory, and the name
|
|
613
|
+
// inside the error carries it, which is what an error handler prints
|
|
579
614
|
const mountRelative = rawPath === "/" && !req.endsWithSlash ? "" : url;
|
|
580
615
|
const statTarget = mountRelative.endsWith("/") && !fullpath.endsWith(path.sep) ? fullpath + path.sep : fullpath;
|
|
581
616
|
if (root && !fullpath.startsWith(resolvedRoot)) {
|
|
@@ -586,14 +621,10 @@ function serveStatic(root, options) {
|
|
|
586
621
|
}
|
|
587
622
|
|
|
588
623
|
// Before the stat, because send judges the path before it looks at the disk: a hidden
|
|
589
|
-
// segment
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
// file, and resolving it away is what tells the two apart
|
|
594
|
-
// and these are the segments path.normalize(url) would have produced, taken off the joined
|
|
595
|
-
// path rather than walked again: the check above has just proved it starts with the root,
|
|
596
|
-
// so what follows the root is the url in normal form
|
|
624
|
+
// segment in a path that does not exist answers what the dotfiles rule says and not the
|
|
625
|
+
// ENOENT the disk would have given. Normalized first, as send normalizes before it judges:
|
|
626
|
+
// a ".." segment is not a hidden file. These are the segments path.normalize(url) would
|
|
627
|
+
// have produced, taken off the joined path rather than walked again
|
|
597
628
|
if (containsDotFile(fullpath.slice(resolvedRoot.length).split(/[\\/]/))) {
|
|
598
629
|
const refusal = options.dotfiles === "deny" ? 403 : options.dotfiles === "allow" ? 0 : 404;
|
|
599
630
|
if (refusal !== 0 && !(options.dotfiles === "ignore_files" && !path.basename(url).startsWith("."))) {
|
|
@@ -668,7 +699,7 @@ function serveStatic(root, options) {
|
|
|
668
699
|
// error handler with its errno, code, syscall and path still on it, and an
|
|
669
700
|
// error handler doing res.send(err) sends those as JSON. Passing the string
|
|
670
701
|
// sent an HTML page instead.
|
|
671
|
-
return next(asStatError(statError));
|
|
702
|
+
return next(asStatError(/** @type {HttpError} */ (statError)));
|
|
672
703
|
} else return next();
|
|
673
704
|
}
|
|
674
705
|
}
|
|
@@ -689,13 +720,11 @@ function serveStatic(root, options) {
|
|
|
689
720
|
if (stat.isDirectory() || req.endsWithSlash) {
|
|
690
721
|
if (!req.endsWithSlash) {
|
|
691
722
|
if (options.redirect) {
|
|
692
|
-
// The query goes along
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
// serve-static locks its redirect page down the way it locks an error page:
|
|
698
|
-
// the body names the target, and the target came from the request
|
|
723
|
+
// The query goes along and the leading slashes are collapsed. Both were wrong:
|
|
724
|
+
// "/docs?page=3" redirected to "/docs/" and lost the page, and "//assets"
|
|
725
|
+
// answered "Location: //assets/", which a browser reads as protocol-relative
|
|
726
|
+
// and follows to the host "assets". serve-static locks its redirect page down
|
|
727
|
+
// the way it locks an error page: the body names a target the request supplied
|
|
699
728
|
res.setHeader("Content-Security-Policy", "default-src 'none'");
|
|
700
729
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
701
730
|
return res.redirect(301, collapseLeadingSlashes(req._originalPath + "/") + req.urlQuery, true);
|
|
@@ -715,7 +744,7 @@ function serveStatic(root, options) {
|
|
|
715
744
|
res.status(404);
|
|
716
745
|
// the fs error, as above: the index file is missing and the error handler
|
|
717
746
|
// is told which one and where
|
|
718
|
-
return next(asStatError(err));
|
|
747
|
+
return next(asStatError(/** @type {HttpError} */ (err)));
|
|
719
748
|
} else return next();
|
|
720
749
|
}
|
|
721
750
|
if (found === null) {
|
|
@@ -786,7 +815,7 @@ function serveStatic(root, options) {
|
|
|
786
815
|
* encoding nobody knows throws, since decoding it wrong is worse than refusing.
|
|
787
816
|
*
|
|
788
817
|
* @param {string|undefined} contentEncoding
|
|
789
|
-
* @returns {
|
|
818
|
+
* @returns {Inflater|false|undefined}
|
|
790
819
|
*/
|
|
791
820
|
function createInflate(contentEncoding) {
|
|
792
821
|
const encoding = (contentEncoding || "identity").toLowerCase();
|
|
@@ -807,21 +836,20 @@ function createInflate(contentEncoding) {
|
|
|
807
836
|
return false;
|
|
808
837
|
}
|
|
809
838
|
// the flag the final flush passes, so a truncated stream errors instead of resolving empty
|
|
810
|
-
/** @type {
|
|
839
|
+
/** @type {Inflater} */ (stream)._finishFlag =
|
|
811
840
|
encoding === "br" ? zlib.constants.BROTLI_OPERATION_FINISH : zlib.constants.Z_FINISH;
|
|
812
841
|
return stream;
|
|
813
842
|
}
|
|
814
843
|
|
|
815
844
|
/**
|
|
816
|
-
* Builds one of the body parsers. All four share the same work
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
* they turn the bytes into.
|
|
845
|
+
* Builds one of the body parsers. All four share the same work: deciding whether this request has
|
|
846
|
+
* a body worth reading, collecting it within the size limit, decompressing it and handing the
|
|
847
|
+
* bytes over. They differ in the content type they claim and in what they turn the bytes into.
|
|
820
848
|
*
|
|
821
849
|
* @param {string} defaultType the type matched when the caller names none
|
|
822
|
-
* @param {
|
|
823
|
-
*
|
|
824
|
-
* @param {(options:
|
|
850
|
+
* @param {BodyHandler} beforeReturn turns the collected bytes into req.body. Called with the
|
|
851
|
+
* request, the response, next, the options, the body and its charset
|
|
852
|
+
* @param {(options: BodyParserOptions) => void} [checkOptions] whatever this parser alone has to check
|
|
825
853
|
* @param {string} [charsetPolicy] which charsets this parser accepts, as body-parser draws the
|
|
826
854
|
* lines: "utf" (json, utf-* only), "urlencoded" (utf-8 and iso-8859-1), "any" (anything iconv
|
|
827
855
|
* knows), or undefined for a parser that never decodes (raw)
|
|
@@ -881,12 +909,12 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
881
909
|
// Whether a content-type is one this parser claims, remembered per parser.
|
|
882
910
|
//
|
|
883
911
|
// Only reached when the caller asked for a wildcard or a list, since a plain type takes the
|
|
884
|
-
// simpleType shortcut above
|
|
885
|
-
//
|
|
886
|
-
//
|
|
912
|
+
// simpleType shortcut above. For those callers type-is was 513ns to reach the same answer
|
|
913
|
+
// about the same string on every request, against 4ns for one already worked out. The
|
|
914
|
+
// header is the client's, so the memo needs its ceiling.
|
|
887
915
|
//
|
|
888
916
|
// typeis.is and not typeis(req, ...): the request form first checks that there is a body,
|
|
889
|
-
//
|
|
917
|
+
// which the caller below has established.
|
|
890
918
|
const claimsType = memoizeByString(
|
|
891
919
|
(contentType) => !!typeis.is(contentType, /** @type {string[]} */ (options.type))
|
|
892
920
|
);
|
|
@@ -908,12 +936,10 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
908
936
|
}
|
|
909
937
|
|
|
910
938
|
// The property goes on the request before anything is decided, and its value stays
|
|
911
|
-
// undefined: body-parser's read() does
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
// refuses the request with a 500 when the property is missing, and tRPC's adapter
|
|
916
|
-
// reads the body itself when it is, so getting either half wrong breaks one of them.
|
|
939
|
+
// undefined: body-parser's read() does the same, and both halves matter. Undefined, so
|
|
940
|
+
// a handler can tell "nothing parsed this" from "the body was empty". Present, because
|
|
941
|
+
// `"body" in req` is how a library asks whether a parser has run: Apollo's express
|
|
942
|
+
// middleware answers 500 when it is missing, and tRPC's adapter reads the body itself
|
|
917
943
|
if (!("body" in req)) {
|
|
918
944
|
req.body = undefined;
|
|
919
945
|
}
|
|
@@ -983,7 +1009,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
983
1009
|
if (lengthNumber === 0) {
|
|
984
1010
|
req.bodyRead = true;
|
|
985
1011
|
const empty = Buffer.alloc(0);
|
|
986
|
-
if (!runVerify(req, res, next, options, empty)) {
|
|
1012
|
+
if (!runVerify(req, res, next, options, empty, encoding)) {
|
|
987
1013
|
return;
|
|
988
1014
|
}
|
|
989
1015
|
return beforeReturn(req, res, next, options, empty, encoding);
|
|
@@ -1013,7 +1039,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1013
1039
|
return next();
|
|
1014
1040
|
}
|
|
1015
1041
|
|
|
1016
|
-
|
|
1042
|
+
/** @type {Inflater|false|undefined} */
|
|
1017
1043
|
let inflate;
|
|
1018
1044
|
let totalSize = 0;
|
|
1019
1045
|
const rawContentEncoding = req._rawHeader("content-encoding");
|
|
@@ -1047,11 +1073,9 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1047
1073
|
next = bindContext(next);
|
|
1048
1074
|
|
|
1049
1075
|
// with nothing to decompress, uWS can collect the whole body in native code: one
|
|
1050
|
-
// callback instead of one per chunk, the limit enforced before any byte reaches JS,
|
|
1051
|
-
//
|
|
1052
|
-
// returns
|
|
1053
|
-
// original case; a chunked body accumulates in the same native vector and only loses
|
|
1054
|
-
// the length check, since there is no declaration to hold it to
|
|
1076
|
+
// callback instead of one per chunk, the limit enforced before any byte reaches JS, and
|
|
1077
|
+
// no copy at all, since the parsers turn the bytes into req.body before the callback
|
|
1078
|
+
// returns. A chunked body uses the same native vector and only loses the length check
|
|
1055
1079
|
const declared = lengthNumber;
|
|
1056
1080
|
const declaresLength = !Number.isNaN(declared) && declared > 0;
|
|
1057
1081
|
if (!req.receivedData && !inflate && req._res.collectBody && (declaresLength || isNaN(declared))) {
|
|
@@ -1084,7 +1108,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1084
1108
|
if (copyBody) {
|
|
1085
1109
|
buf = Buffer.from(buf);
|
|
1086
1110
|
}
|
|
1087
|
-
if (!runVerify(req, res, next, options, buf)) {
|
|
1111
|
+
if (!runVerify(req, res, next, options, buf, encoding)) {
|
|
1088
1112
|
return;
|
|
1089
1113
|
}
|
|
1090
1114
|
beforeReturn(req, res, next, options, buf, encoding);
|
|
@@ -1093,12 +1117,11 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1093
1117
|
}
|
|
1094
1118
|
|
|
1095
1119
|
// uWS neuters its ArrayBuffer after the callback, so every chunk has to be copied out of
|
|
1096
|
-
// it
|
|
1097
|
-
// known and we
|
|
1098
|
-
// straight into one buffer and the body is copied once.
|
|
1099
|
-
//
|
|
1100
|
-
|
|
1101
|
-
// already rejected above
|
|
1120
|
+
// it, and then Buffer.concat copied the whole body a second time. When content-length is
|
|
1121
|
+
// known and we are not inflating, the final size is known up front, so chunks go
|
|
1122
|
+
// straight into one buffer and the body is copied once. The cap means a client that
|
|
1123
|
+
// declares a body and never sends it costs no more than one that sends it
|
|
1124
|
+
const abs = [];
|
|
1102
1125
|
const declaredLength = inflate ? -1 : Number(length);
|
|
1103
1126
|
let target =
|
|
1104
1127
|
declaredLength > 0 && declaredLength <= MAX_PREALLOCATED_BODY
|
|
@@ -1116,16 +1139,15 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1116
1139
|
/**
|
|
1117
1140
|
* A zlib throw becomes the 400 body-parser answers a corrupt body with.
|
|
1118
1141
|
*
|
|
1119
|
-
* zlib reports it twice: process() throws, and the stream emits 'error' a tick
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
* throw, since process() would have removed it.
|
|
1142
|
+
* zlib reports it twice: process() throws, and the stream emits 'error' a tick later.
|
|
1143
|
+
* fast-zlib removes its own listeners on the way out, so that second one lands on
|
|
1144
|
+
* nothing, and an unhandled 'error' event ends the process. The listener goes on after
|
|
1145
|
+
* the throw, since process() would have removed it.
|
|
1124
1146
|
*
|
|
1125
|
-
* @param {
|
|
1147
|
+
* @param {HttpError} err what inflate.process threw
|
|
1126
1148
|
*/
|
|
1127
1149
|
function failInflate(err) {
|
|
1128
|
-
/** @type {
|
|
1150
|
+
/** @type {Inflater} */ (inflate).instance?.on?.("error", () => {});
|
|
1129
1151
|
finished = true;
|
|
1130
1152
|
abs.length = 0;
|
|
1131
1153
|
target = null;
|
|
@@ -1178,7 +1200,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1178
1200
|
* finished flag matters: uWS goes on delivering chunks after an oversized body has
|
|
1179
1201
|
* been refused, and without it every further chunk would answer the request again.
|
|
1180
1202
|
*
|
|
1181
|
-
* @param {
|
|
1203
|
+
* @param {Buffer|ArrayBuffer} buf a Buffer, or an ArrayBuffer straight from uWS
|
|
1182
1204
|
*/
|
|
1183
1205
|
function onData(buf) {
|
|
1184
1206
|
if (finished) {
|
|
@@ -1193,7 +1215,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1193
1215
|
} catch (e) {
|
|
1194
1216
|
// a body that does not decompress is the client's mistake, and zlib
|
|
1195
1217
|
// throwing here used to escape into whatever called us
|
|
1196
|
-
return failInflate(e);
|
|
1218
|
+
return failInflate(/** @type {HttpError} */ (e));
|
|
1197
1219
|
}
|
|
1198
1220
|
}
|
|
1199
1221
|
|
|
@@ -1214,7 +1236,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1214
1236
|
try {
|
|
1215
1237
|
tail = inflate.process(EMPTY_BUFFER, inflate._finishFlag);
|
|
1216
1238
|
} catch (e) {
|
|
1217
|
-
return failInflate(e);
|
|
1239
|
+
return failInflate(/** @type {HttpError} */ (e));
|
|
1218
1240
|
}
|
|
1219
1241
|
if (tail.length && !keepChunk(tail)) {
|
|
1220
1242
|
return;
|
|
@@ -1242,7 +1264,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1242
1264
|
: abs.length === 1
|
|
1243
1265
|
? abs[0]
|
|
1244
1266
|
: Buffer.concat(abs);
|
|
1245
|
-
if (!runVerify(req, res, next, options, buf)) {
|
|
1267
|
+
if (!runVerify(req, res, next, options, buf, encoding)) {
|
|
1246
1268
|
return;
|
|
1247
1269
|
}
|
|
1248
1270
|
beforeReturn(req, res, next, options, buf, encoding);
|
|
@@ -1290,7 +1312,7 @@ const json = createBodyParser(
|
|
|
1290
1312
|
// either: it decodes through iconv, which strips it, so by the time the first character is
|
|
1291
1313
|
// looked at the mark is gone. JSON.parse would refuse it, so without this a body saved by
|
|
1292
1314
|
// an editor that writes a BOM is answered 400 here and 200 by Express.
|
|
1293
|
-
const text = stripBom(decodeBody(buf, encoding));
|
|
1315
|
+
const text = stripBom(decodeBody(buf, /** @type {string} */ (encoding)));
|
|
1294
1316
|
|
|
1295
1317
|
// "strict" means only an object or an array is a body, which is body-parser's default and
|
|
1296
1318
|
// was not honoured here at all: the check read req.body before this function had parsed
|
|
@@ -1318,7 +1340,7 @@ const json = createBodyParser(
|
|
|
1318
1340
|
} catch (e) {
|
|
1319
1341
|
// V8's own error, which is what body-parser hands on: its message says where the parse
|
|
1320
1342
|
// gave up, and it is still the SyntaxError an application may be testing for
|
|
1321
|
-
const err = /** @type {
|
|
1343
|
+
const err = /** @type {SyntaxError} */ (e);
|
|
1322
1344
|
return next(asBodyError(err, 400, "entity.parse.failed", { body: text }));
|
|
1323
1345
|
}
|
|
1324
1346
|
|
|
@@ -1345,7 +1367,7 @@ const text = createBodyParser(
|
|
|
1345
1367
|
"text/plain",
|
|
1346
1368
|
function (req, res, next, options, buf, encoding) {
|
|
1347
1369
|
try {
|
|
1348
|
-
req.body = decodeBody(buf, encoding);
|
|
1370
|
+
req.body = decodeBody(buf, /** @type {string} */ (encoding));
|
|
1349
1371
|
} catch (e) {
|
|
1350
1372
|
return next(e);
|
|
1351
1373
|
}
|
|
@@ -1387,7 +1409,7 @@ const urlencoded = createBodyParser(
|
|
|
1387
1409
|
"application/x-www-form-urlencoded",
|
|
1388
1410
|
function (req, res, next, options, buf, encoding) {
|
|
1389
1411
|
try {
|
|
1390
|
-
const body = decodeBody(buf, encoding);
|
|
1412
|
+
const body = decodeBody(buf, /** @type {string} */ (encoding));
|
|
1391
1413
|
// Express 5 defaults extended to false, so nested keys need opting in
|
|
1392
1414
|
const extended = typeof options.extended !== "undefined" ? options.extended : false;
|
|
1393
1415
|
// qs has to know the charset itself for anything but utf-8, and the sentinel options
|
|
@@ -1403,7 +1425,8 @@ const urlencoded = createBodyParser(
|
|
|
1403
1425
|
}
|
|
1404
1426
|
req.body = parsed;
|
|
1405
1427
|
} else {
|
|
1406
|
-
|
|
1428
|
+
// settled by the check below the parser, so it is a number by the time a body arrives
|
|
1429
|
+
const count = parameterCount(body, /** @type {number} */ (options.parameterLimit));
|
|
1407
1430
|
if (count === undefined) {
|
|
1408
1431
|
return next(bodyError("too many parameters", 413, "parameters.too.many"));
|
|
1409
1432
|
}
|
|
@@ -1417,7 +1440,7 @@ const urlencoded = createBodyParser(
|
|
|
1417
1440
|
arrayLimit: Math.max(100, count + 1),
|
|
1418
1441
|
charsetSentinel: options.charsetSentinel,
|
|
1419
1442
|
interpretNumericEntities: options.interpretNumericEntities,
|
|
1420
|
-
charset: encoding,
|
|
1443
|
+
charset: /** @type {"utf-8"|"iso-8859-1"} */ (encoding),
|
|
1421
1444
|
parameterLimit: options.parameterLimit
|
|
1422
1445
|
};
|
|
1423
1446
|
req.body = needsQs
|
|
@@ -1435,7 +1458,7 @@ const urlencoded = createBodyParser(
|
|
|
1435
1458
|
strictDepth: true,
|
|
1436
1459
|
charsetSentinel: options.charsetSentinel,
|
|
1437
1460
|
interpretNumericEntities: options.interpretNumericEntities,
|
|
1438
|
-
charset: encoding,
|
|
1461
|
+
charset: /** @type {"utf-8"|"iso-8859-1"} */ (encoding),
|
|
1439
1462
|
parameterLimit: options.parameterLimit
|
|
1440
1463
|
})
|
|
1441
1464
|
);
|