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.
@@ -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, and
79
- // forwards everything after that point. The list is the same thing said from the other side, since
80
- // by the time this hands over, the file has been found and stat'ed already: what is left to fail
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, and that is the point of the list. Both are about a file that
84
- // exists and about conditions the client itself set, and falling through swallowed them: a Range
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 {any}
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 {any} req
211
- * @param {any} res
212
- * @param {(err?: any) => void} next
213
- * @param {any} options
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
- options.verify(req, res, buf);
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
- const err = /** @type {any} */ (e);
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 goes to some trouble over this: it builds a string that is the body up to the
240
- * offending character followed by placeholder characters, asks JSON.parse to fail on that, and
241
- * then puts the real characters back into whatever V8 said. The point is that an application
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 {any} */ (e).message.replace(/#+/g, (/** @type {string} */ placeholder) =>
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
- const err = /** @type {any} */ (new Error(message));
287
- if (BODY_ERROR_NAMES[status]) {
288
- err.name = BODY_ERROR_NAMES[status];
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 {any} err
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 the
365
- * Last-Modified of a variant are its own, which is the whole point. Two bodies sharing one ETag is
366
- * how a shared cache ends up handing brotli to a client that cannot read it.
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, and none at all when the last request already found there is
369
- * no twin to have. See twinCache above for what is remembered and what is not.
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: any, name: string, candidate: string}|null} the file to serve, or null
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: any, res: any, next: (err?: any) => void) => any}
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 = /** @type {any} */ (
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 {any} */ (cache))
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 "/" where nothing has left anywhere. Absolute because resolvedRoot is, so
559
- // nothing here resolves against the working directory per request either.
560
- // and without the trailing separator join keeps and resolve does not, because statTarget
561
- // below puts it back only where it belongs: linux refuses a file asked for as a directory,
562
- // so a mount whose root is a file answers nothing at all if the separator stays here.
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 at all.
609
+ // would ask the disk for a directory and could never answer.
574
610
  //
575
- // Send then stats `normalize(join(root, path))`, and both of those keep a trailing
576
- // separator where `resolve` takes it off. The separator is not decoration: the disk refuses
577
- // a file that is asked for as a directory, and the name inside the error carries it, which
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 anywhere in a path that does not exist answers what the dotfiles rule says and
590
- // not the ENOENT the disk would have given. sendFile applies the same rule below, and
591
- // reaches it only for paths that do exist.
592
- // normalized first, as send normalizes before it judges: a ".." segment is not a hidden
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, and the leading slashes are collapsed. Both were
693
- // wrong: "/docs?page=3" redirected to "/docs/" and lost the page, and a
694
- // request for "//assets" answered "Location: //assets/", which a browser
695
- // reads as a protocol-relative URL and follows to the host "assets". A
696
- // redirect that leaves this server is not a redirect this server meant.
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 {any|undefined}
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 {any} */ (stream)._finishFlag =
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, which is deciding whether this
817
- * request has a body worth reading, collecting it within the size limit, decompressing it and
818
- * handing the bytes over; they differ only in the content type they claim by default and in what
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 {(...args: any[]) => any} beforeReturn turns the collected bytes into req.body. Called
823
- * with the request, the response, next, the options, the body and its charset
824
- * @param {(options: any) => void} [checkOptions] whatever this parser alone has to check
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 and never calls type-is at all. For those callers type-is was
885
- // 513 ns to reach the same answer about the same string on every request, against 4 ns for
886
- // an answer already worked out. The header is the client's, so the memo needs its ceiling.
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
- // and the caller below has established that already.
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 exactly this, and the two halves both matter.
912
- // Undefined, so a handler can still tell "nothing parsed this" from "the body was
913
- // empty", which seeding an empty object would lose. Present, because `"body" in req`
914
- // is how a library asks whether a parser has run at all: Apollo's express middleware
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
- const abs = [];
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
- // and no copy at all - the parsers turn the bytes into req.body before the callback
1052
- // returns, so a view over uWS's own memory is enough. A declared length was the
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 - and then Buffer.concat copied the whole body a second time. when content-length is
1097
- // known and we aren't inflating, the final size is known up front, so chunks can go
1098
- // straight into one buffer and the body is copied once.
1099
- // the cap means a client that declares a body and never sends it costs no more than one
1100
- // that actually sends a body that size, and content-length above limit was
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
- * later. fast-zlib removes its own listeners on the way out, so that second one
1121
- * lands on nothing, and an unhandled 'error' event ends the process: a corrupt
1122
- * gzip body was enough to take the server down. The listener goes on after the
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 {any} err what inflate.process threw
1147
+ * @param {HttpError} err what inflate.process threw
1126
1148
  */
1127
1149
  function failInflate(err) {
1128
- /** @type {any} */ (inflate).instance?.on?.("error", () => {});
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 {any} buf a Buffer, or an ArrayBuffer straight from uWS
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 {any} */ (e);
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
- const count = parameterCount(body, options.parameterLimit);
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
  );